summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Simon Bowden <sbowden@google.com> 2023-07-31 14:35:32 +0000
committer Simon Bowden <sbowden@google.com> 2023-08-02 10:42:04 +0000
commit549920720ffc7b29adc13041492d6116fa391077 (patch)
tree02fed6d5aa741e8e5c46bc7c16eb344bab666bcd
parentd3f21133a08dc21d949f676517235ef5a86bd369 (diff)
Add back the older Ringtone implementation to allow flag control.
This CL splits the Ringtone class into RingtoneV1 and RingtoneV2, with a switch between them. The switch is currently a system property as the flag system for frameworks/base is still being built. Unfortunately the old implementation didn't have test coverage. Bug: 293846645 Test: presubmit, manual Change-Id: I0a7fd10261e29a74ea1798f5bfbf8eddcafa1e5e
-rw-r--r--media/java/android/media/IRingtonePlayer.aidl12
-rw-r--r--media/java/android/media/LocalRingtonePlayer.java10
-rw-r--r--media/java/android/media/Ringtone.java527
-rw-r--r--media/java/android/media/RingtoneManager.java59
-rw-r--r--media/java/android/media/RingtoneV1.java614
-rw-r--r--media/java/android/media/RingtoneV2.java690
-rw-r--r--packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java69
8 files changed, 1550 insertions, 439 deletions
diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl
index b3f72a1aa2bb..73f15f21596c 100644
--- a/media/java/android/media/IRingtonePlayer.aidl
+++ b/media/java/android/media/IRingtonePlayer.aidl
@@ -30,12 +30,20 @@ interface IRingtonePlayer {
/** Used for Ringtone.java playback */
@UnsupportedAppUsage
oneway void play(IBinder token, in Uri uri, in AudioAttributes aa, float volume, boolean looping);
+ oneway void stop(IBinder token);
+ boolean isPlaying(IBinder token);
+
+ // RingtoneV1
+ oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
+ float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
+ oneway void setPlaybackProperties(IBinder token, float volume, boolean looping,
+ boolean hapticGeneratorEnabled);
+
+ // RingtoneV2
oneway void playRemoteRingtone(IBinder token, in Uri uri, in AudioAttributes aa,
boolean useExactAudioAttributes, int enabledMedia, in @nullable VibrationEffect ve,
float volume, boolean looping, boolean hapticGeneratorEnabled,
in @nullable VolumeShaper.Configuration volumeShaperConfig);
- oneway void stop(IBinder token);
- boolean isPlaying(IBinder token);
oneway void setLooping(IBinder token, boolean looping);
oneway void setVolume(IBinder token, float volume);
oneway void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled);
diff --git a/media/java/android/media/LocalRingtonePlayer.java b/media/java/android/media/LocalRingtonePlayer.java
index d0169b9e2501..fe7cc3ec2af3 100644
--- a/media/java/android/media/LocalRingtonePlayer.java
+++ b/media/java/android/media/LocalRingtonePlayer.java
@@ -37,7 +37,7 @@ import java.util.Objects;
* @hide
*/
public class LocalRingtonePlayer
- implements Ringtone.RingtonePlayer, MediaPlayer.OnCompletionListener {
+ implements RingtoneV2.RingtonePlayer, MediaPlayer.OnCompletionListener {
private static final String TAG = "LocalRingtonePlayer";
// keep references on active Ringtones until stopped or completion listener called.
@@ -45,7 +45,7 @@ public class LocalRingtonePlayer
private final MediaPlayer mMediaPlayer;
private final AudioAttributes mAudioAttributes;
- private final Ringtone.RingtonePlayer mVibrationPlayer;
+ private final RingtoneV2.RingtonePlayer mVibrationPlayer;
private final Ringtone.Injectables mInjectables;
private final AudioManager mAudioManager;
private final VolumeShaper mVolumeShaper;
@@ -55,7 +55,7 @@ public class LocalRingtonePlayer
@NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables,
@NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator,
@Nullable VolumeShaper volumeShaper,
- @Nullable Ringtone.RingtonePlayer vibrationPlayer) {
+ @Nullable RingtoneV2.RingtonePlayer vibrationPlayer) {
Objects.requireNonNull(mediaPlayer);
Objects.requireNonNull(audioAttributes);
Objects.requireNonNull(injectables);
@@ -74,7 +74,7 @@ public class LocalRingtonePlayer
* loaded in the local player.
*/
@Nullable
- static Ringtone.RingtonePlayer create(@NonNull Context context,
+ static RingtoneV2.RingtonePlayer create(@NonNull Context context,
@NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
@NonNull Uri soundUri,
@NonNull AudioAttributes audioAttributes,
@@ -311,7 +311,7 @@ public class LocalRingtonePlayer
}
/** A RingtonePlayer that only plays a VibrationEffect. */
- static class VibrationEffectPlayer implements Ringtone.RingtonePlayer {
+ static class VibrationEffectPlayer implements RingtoneV2.RingtonePlayer {
private static final int VIBRATION_LOOP_DELAY_MS = 200;
private final VibrationEffect mVibrationEffect;
private final VibrationAttributes mVibrationAttributes;
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 3a6b39834851..0319f32521c1 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -25,14 +25,11 @@ import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -43,7 +40,6 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -100,68 +96,70 @@ public class Ringtone {
private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
+ MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
- private final Context mContext;
- private final Vibrator mVibrator;
- private final AudioManager mAudioManager;
- private VolumeShaper.Configuration mVolumeShaperConfig;
+ // Flag-selected ringtone implementation to use.
+ private final ApiInterface mApiImpl;
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ public Ringtone(Context context, boolean allowRemote) {
+ mApiImpl = new RingtoneV1(context, allowRemote);
+ }
/**
- * Flag indicating if we're allowed to fall back to remote playback using
- * {@link #mRemoteRingtoneService}. Typically this is false when we're the remote
- * player and there is nobody else to delegate to.
+ * Constructor for legacy V1 initialization paths using non-public APIs on RingtoneV1.
*/
- private final boolean mAllowRemote;
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Injectables mInjectables;
-
- private final int mEnabledMedia;
-
- private final Uri mUri;
- private String mTitle;
-
- private AudioAttributes mAudioAttributes;
- private boolean mUseExactAudioAttributes;
- private boolean mPreferBuiltinDevice;
- private RingtonePlayer mActivePlayer;
- // playback properties, use synchronized with mPlaybackSettingsLock
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
- private final Object mPlaybackSettingsLock = new Object();
- private final VibrationEffect mVibrationEffect;
+ private Ringtone(RingtoneV1 ringtoneV1) {
+ mApiImpl = ringtoneV1;
+ }
private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia,
@NonNull AudioAttributes effectiveAudioAttributes,
@Nullable VibrationEffect effectiveVibrationEffect,
boolean effectiveHapticGeneratorEnabled) {
- // Context
- mContext = builder.mContext;
- mInjectables = builder.mInjectables;
- //mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- mAudioManager = mContext.getSystemService(AudioManager.class);
- mRemoteRingtoneService = builder.mAllowRemote ? mAudioManager.getRingtonePlayer() : null;
- mVibrator = mContext.getSystemService(Vibrator.class);
-
- // Local-only (not propagated to remote).
- mPreferBuiltinDevice = builder.mPreferBuiltinDevice; // System-only
- mAllowRemote = (mRemoteRingtoneService != null); // Always false for remote.
-
- // Properties potentially propagated to remote player.
- mEnabledMedia = effectiveEnabledMedia;
- mUri = builder.mUri;
- mVolumeShaperConfig = builder.mVolumeShaperConfig;
- mVolume = builder.mInitialSoundVolume;
- mIsLooping = builder.mLooping;
- mVibrationEffect = effectiveVibrationEffect;
- mAudioAttributes = effectiveAudioAttributes;
- mUseExactAudioAttributes = builder.mUseExactAudioAttributes;
- mHapticGeneratorEnabled = 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 mEnabledMedia;
+ return mApiImpl.getEnabledMedia();
}
/**
@@ -172,15 +170,7 @@ public class Ringtone {
*/
@Deprecated
public void setStreamType(int streamType) {
- setAudioAttributes(
- getAudioAttributesForLegacyStreamType(streamType, "setStreamType()"));
- }
-
- private AudioAttributes getAudioAttributesForLegacyStreamType(int streamType, String originOp) {
- PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", originOp);
- return new AudioAttributes.Builder()
- .setInternalLegacyStreamType(streamType)
- .build();
+ mApiImpl.setStreamType(streamType);
}
/**
@@ -192,7 +182,7 @@ public class Ringtone {
*/
@Deprecated
public int getStreamType() {
- return AudioAttributes.toLegacyStreamType(mAudioAttributes);
+ return mApiImpl.getStreamType();
}
/**
@@ -201,17 +191,7 @@ public class Ringtone {
*/
public void setAudioAttributes(AudioAttributes attributes)
throws IllegalArgumentException {
- // TODO: deprecate this method - it will be done with a builder.
- if (attributes == null) {
- throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
- }
- mAudioAttributes = attributes;
- // Setting the audio attributes requires re-initializing the player.
- if (mActivePlayer != null) {
- // The audio attributes have to be set before the media player is prepared.
- // Re-initialize it.
- reinitializeActivePlayer();
- }
+ mApiImpl.setAudioAttributes(attributes);
}
/**
@@ -221,19 +201,19 @@ public class Ringtone {
*/
@Nullable
public VibrationEffect getVibrationEffect() {
- return mVibrationEffect;
+ return mApiImpl.getVibrationEffect();
}
/** @hide */
@VisibleForTesting
public boolean getPreferBuiltinDevice() {
- return mPreferBuiltinDevice;
+ return mApiImpl.getPreferBuiltinDevice();
}
/** @hide */
@VisibleForTesting
public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mVolumeShaperConfig;
+ return mApiImpl.getVolumeShaperConfig();
}
/**
@@ -243,31 +223,13 @@ public class Ringtone {
*/
@VisibleForTesting
public boolean isLocalOnly() {
- return !mAllowRemote;
+ return mApiImpl.isLocalOnly();
}
/** @hide */
@VisibleForTesting
public boolean isUsingRemotePlayer() {
- return mActivePlayer instanceof RemoteRingtonePlayer;
- }
-
- /**
- * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
- * the one on which outgoing audio for SIM calls is played.
- *
- * @param audioManager the audio manage.
- * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
- * none can be found.
- */
- private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
- AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : deviceList) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- return device;
- }
- }
- return null;
+ return mApiImpl.isUsingRemotePlayer();
}
/**
@@ -277,84 +239,7 @@ public class Ringtone {
* @hide
*/
public boolean reinitializeActivePlayer() {
- // Try creating a local media player, or fallback to creating a remote one.
- Trace.beginSection("reinitializeActivePlayer");
- try {
- if (mActivePlayer != null) {
- // This would only happen if calling the deprecated setAudioAttributes after
- // building the Ringtone.
- stopAndReleaseActivePlayer();
- }
-
- boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION;
- // Vibration can come from the audio file if using haptic generator or if haptic
- // channels are a possibility.
- boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported()
- && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted());
-
- // VibrationEffect only, use the simplified player without checking for haptic channels.
- if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) {
- mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer(
- mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping);
- return true;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- if (mUri != null) {
- mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri,
- mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables,
- mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping,
- mVolume);
- } else {
- // Using the remote player won't help play a null Uri. Revert straight to fallback.
- // The vibration-only case was already covered above.
- mActivePlayer = createFallbackRingtonePlayer();
- // Fall through to attempting remote fallback play if null.
- }
-
- if (mActivePlayer == null && mAllowRemote) {
- mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume);
- }
-
- return mActivePlayer != null;
- } finally {
- Trace.endSection();
- }
- }
-
- @Nullable
- private LocalRingtonePlayer createFallbackRingtonePlayer() {
- int ringtoneType = RingtoneManager.getDefaultType(mUri);
- if (ringtoneType != -1
- && RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
- Log.w(TAG, "not playing fallback for " + mUri);
- return null;
- }
- // Default ringtone, try fallback ringtone.
- try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
- com.android.internal.R.raw.fallbackring)) {
- if (afd == null) {
- Log.e(TAG, "Could not load fallback ringtone");
- return null;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- return LocalRingtonePlayer.createForFallback(mAudioManager, mVibrator, afd,
- mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig,
- preferredDevice, mIsLooping, mVolume);
- } catch (NotFoundException nfe) {
- Log.e(TAG, "Fallback ringtone does not exist");
- return null;
- } catch (IOException e) {
- // As with the above messages, not including much information about the
- // failure so as not to expose details of the fallback ringtone resource.
- Log.e(TAG, "Exception reading fallback ringtone");
- return null;
- }
+ return mApiImpl.reinitializeActivePlayer();
}
/**
@@ -362,7 +247,7 @@ public class Ringtone {
* @hide
*/
public boolean hasHapticChannels() {
- return (mActivePlayer == null) ? false : mActivePlayer.hasHapticChannels();
+ return mApiImpl.hasHapticChannels();
}
/**
@@ -371,7 +256,7 @@ public class Ringtone {
* {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
*/
public AudioAttributes getAudioAttributes() {
- return mAudioAttributes;
+ return mApiImpl.getAudioAttributes();
}
/**
@@ -379,12 +264,7 @@ public class Ringtone {
* @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
- synchronized (mPlaybackSettingsLock) {
- mIsLooping = looping;
- if (mActivePlayer != null) {
- mActivePlayer.setLooping(looping);
- }
- }
+ mApiImpl.setLooping(looping);
}
/**
@@ -392,9 +272,7 @@ public class Ringtone {
* @return true if this player loops when playing.
*/
public boolean isLooping() {
- synchronized (mPlaybackSettingsLock) {
- return mIsLooping;
- }
+ return mApiImpl.isLooping();
}
/**
@@ -403,22 +281,7 @@ public class Ringtone {
* corresponds to no attenuation being applied.
*/
public void setVolume(float volume) {
- // Ignore if sound not enabled.
- if ((mEnabledMedia & MEDIA_SOUND) == 0) {
- return;
- }
- if (volume < 0.0f) {
- volume = 0.0f;
- } else if (volume > 1.0f) {
- volume = 1.0f;
- }
-
- synchronized (mPlaybackSettingsLock) {
- mVolume = volume;
- if (mActivePlayer != null) {
- mActivePlayer.setVolume(volume);
- }
- }
+ mApiImpl.setVolume(volume);
}
/**
@@ -426,9 +289,7 @@ public class Ringtone {
* @return a value between 0.0f and 1.0f.
*/
public float getVolume() {
- synchronized (mPlaybackSettingsLock) {
- return mVolume;
- }
+ return mApiImpl.getVolume();
}
/**
@@ -439,16 +300,7 @@ public class Ringtone {
* @see android.media.audiofx.HapticGenerator#isAvailable()
*/
public boolean setHapticGeneratorEnabled(boolean enabled) {
- if (!mInjectables.isHapticGeneratorAvailable()) {
- return false;
- }
- synchronized (mPlaybackSettingsLock) {
- mHapticGeneratorEnabled = enabled;
- if (mActivePlayer != null) {
- mActivePlayer.setHapticGeneratorEnabled(enabled);
- }
- }
- return true;
+ return mApiImpl.setHapticGeneratorEnabled(enabled);
}
/**
@@ -456,9 +308,7 @@ public class Ringtone {
* @return true if the HapticGenerator is enabled.
*/
public boolean isHapticGeneratorEnabled() {
- synchronized (mPlaybackSettingsLock) {
- return mHapticGeneratorEnabled;
- }
+ return mApiImpl.isHapticGeneratorEnabled();
}
/**
@@ -468,8 +318,7 @@ public class Ringtone {
* @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);
+ return mApiImpl.getTitle(context);
}
/**
@@ -546,38 +395,21 @@ public class Ringtone {
/** {@hide} */
@UnsupportedAppUsage
public Uri getUri() {
- return mUri;
+ return mApiImpl.getUri();
}
/**
* Plays the ringtone.
*/
public void play() {
- if (mActivePlayer != null) {
- if (mActivePlayer.play()) {
- return;
- } else {
- // Discard active player: play() is only meant to be called once.
- stopAndReleaseActivePlayer();
- }
- }
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Neither local nor remote playback available");
- }
+ mApiImpl.play();
}
/**
* Stops a playing ringtone.
*/
public void stop() {
- stopAndReleaseActivePlayer();
- }
-
- private void stopAndReleaseActivePlayer() {
- if (mActivePlayer != null) {
- mActivePlayer.stopAndRelease();
- mActivePlayer = null;
- }
+ mApiImpl.stop();
}
/**
@@ -586,41 +418,7 @@ public class Ringtone {
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
- if (mActivePlayer != null) {
- return mActivePlayer.isPlaying();
- } else {
- Log.w(TAG, "No active ringtone player");
- return false;
- }
- }
-
- /**
- * Fallback during the play stage rather than initialization, typically due to an issue
- * communicating with the remote player.
- */
- private boolean playFallbackRingtone() {
- if (mActivePlayer != null) {
- Log.wtf(TAG, "Playing fallback ringtone with another active player");
- stopAndReleaseActivePlayer();
- }
- int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
- if (mAudioManager.getStreamVolume(streamType) == 0) {
- // TODO: Return true? If volume is off, this is a successful play.
- return false;
- }
- mActivePlayer = createFallbackRingtonePlayer();
- if (mActivePlayer == null) {
- return false; // the create method logs if it returns null.
- } else if (mActivePlayer.play()) {
- return true;
- } else {
- stopAndReleaseActivePlayer();
- return false;
- }
- }
-
- void setTitle(String title) {
- mTitle = title;
+ return mApiImpl.isPlaying();
}
/**
@@ -887,140 +685,6 @@ public class Ringtone {
}
/**
- * Play a specific ringtone. This interface is implemented by either local (this process) or
- * proxied-remote playback via AudioManager.getRingtonePlayer, so that the caller
- * (Ringtone class) can just use a single player after the initial creation.
- * @hide
- */
- interface RingtonePlayer {
- /**
- * Start playing the ringtone, returning false if there was a problem that
- * requires falling back to the fallback ringtone resource.
- */
- boolean play();
- boolean isPlaying();
- void stopAndRelease();
-
- // Mutating playback methods.
- void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo);
- void setLooping(boolean looping);
- void setHapticGeneratorEnabled(boolean enabled);
- void setVolume(float volume);
-
- boolean hasHapticChannels();
- }
-
- /**
- * Remote RingtonePlayer. All operations are delegated via the IRingtonePlayer interface, which
- * should ultimately be backed by a RingtoneLocalPlayer within the system services.
- */
- static class RemoteRingtonePlayer implements RingtonePlayer {
- private final IBinder mRemoteToken = new Binder();
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Uri mCanonicalUri;
- private final int mEnabledMedia;
- private final VibrationEffect mVibrationEffect;
- private final VolumeShaper.Configuration mVolumeShaperConfig;
- private final AudioAttributes mAudioAttributes;
- private final boolean mUseExactAudioAttributes;
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
-
- RemoteRingtonePlayer(@NonNull IRingtonePlayer remoteRingtoneService,
- @NonNull Uri uri, @NonNull AudioAttributes audioAttributes,
- boolean useExactAudioAttributes,
- @RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
- mRemoteRingtoneService = remoteRingtoneService;
- mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri();
- mAudioAttributes = audioAttributes;
- mUseExactAudioAttributes = useExactAudioAttributes;
- mEnabledMedia = enabledMedia;
- mVibrationEffect = vibrationEffect;
- mVolumeShaperConfig = volumeShaperConfig;
- mHapticGeneratorEnabled = hapticGeneratorEnabled;
- mIsLooping = initialIsLooping;
- mVolume = initialVolume;
- }
-
- @Override
- public boolean play() {
- try {
- mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig);
- return true;
- } catch (RemoteException e) {
- Log.w(TAG, "Problem playing ringtone: " + e);
- return false;
- }
- }
-
- @Override
- public boolean isPlaying() {
- try {
- return mRemoteRingtoneService.isPlaying(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem checking ringtone isPlaying: " + e);
- return false;
- }
- }
-
- @Override
- public void stopAndRelease() {
- try {
- mRemoteRingtoneService.stop(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem stopping ringtone: " + e);
- }
- }
-
- @Override
- public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
- // un-implemented for remote (but not used outside system).
- }
-
- @Override
- public void setLooping(boolean looping) {
- mIsLooping = looping;
- try {
- mRemoteRingtoneService.setLooping(mRemoteToken, looping);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting looping: " + e);
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- mHapticGeneratorEnabled = enabled;
- try {
- mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting hapticGeneratorEnabled: " + e);
- }
- }
-
- @Override
- public void setVolume(float volume) {
- mVolume = volume;
- try {
- mRemoteRingtoneService.setVolume(mRemoteToken, volume);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting volume: " + e);
- }
- }
-
- @Override
- public boolean hasHapticChannels() {
- // FIXME: support remote player, or internalize haptic channels support and remove
- // entirely.
- return false;
- }
- }
-
- /**
* Interface for intercepting static methods and constructors, for unit testing only.
* @hide
*/
@@ -1071,4 +735,47 @@ public class Ringtone {
}
}
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Switch for using the new ringtone implementation (RingtoneV1 vs RingtoneV2). This may be
+ * called from both system server and app-side sdk.
+ *
+ * @hide
+ */
+ public static boolean useRingtoneV2() {
+ // TODO(b/293846645): chang eto new flagging infra
+ return SystemProperties.getBoolean("persist.audio.ringtone.use_v2", false);
+ }
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 12766fbb6c9d..0ad8c24b10c6 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -496,13 +496,32 @@ public class RingtoneManager {
mPreviousRingtone.stop();
}
- mPreviousRingtone = new Ringtone.Builder(
- mContext, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(mType))
- .setUri(getRingtoneUri(position))
- .build();
+ Ringtone ringtone;
+ Uri positionUri = getRingtoneUri(position);
+ if (Ringtone.useRingtoneV2()) {
+ mPreviousRingtone = new Ringtone.Builder(
+ mContext, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(mType))
+ .setUri(positionUri)
+ .build();
+ } else {
+ mPreviousRingtone = createRingtoneV1WithStreamType(mContext, positionUri,
+ inferStreamType(), /* volumeShaperConfig= */ null);
+ }
return mPreviousRingtone;
}
+ private static Ringtone createRingtoneV1WithStreamType(
+ final Context context, Uri ringtoneUri, int streamType,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ try {
+ return Ringtone.createV1WithCustomStreamType(context, streamType, ringtoneUri,
+ volumeShaperConfig);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ }
+ return null;
+ }
+
/**
* Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
*
@@ -694,9 +713,14 @@ public class RingtoneManager {
* @return A {@link Ringtone} for the given URI, or null.
*/
public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
- return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(-1))
- .setUri(ringtoneUri)
- .build();
+ if (Ringtone.useRingtoneV2()) {
+ return new Ringtone.Builder(
+ context, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(-1))
+ .setUri(ringtoneUri)
+ .build();
+ } else {
+ return createRingtoneV1WithStreamType(context, ringtoneUri, -1, null);
+ }
}
/**
@@ -706,11 +730,22 @@ public class RingtoneManager {
@Nullable VolumeShaper.Configuration volumeShaperConfig,
AudioAttributes audioAttributes) {
// TODO: move caller(s) away from this method: inline the builder call.
- return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(ringtoneUri)
- .setVolumeShaperConfig(volumeShaperConfig)
- .setUseExactAudioAttributes(true) // May be using audio-coupled via attrs
- .build();
+ if (Ringtone.useRingtoneV2()) {
+ return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes)
+ .setUri(ringtoneUri)
+ .setVolumeShaperConfig(volumeShaperConfig)
+ .setUseExactAudioAttributes(true) // May be using audio-coupled via attrs
+ .build();
+ } else {
+ try {
+ return Ringtone.createV1WithCustomAudioAttributes(context, audioAttributes,
+ ringtoneUri, volumeShaperConfig, /* allowRemote= */ true);
+ } catch (Exception ex) {
+ // Match broad catching of createRingtoneV1.
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ return null;
+ }
+ }
}
/**
diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java
new file mode 100644
index 000000000000..3c54d4a0d166
--- /dev/null
+++ b/media/java/android/media/RingtoneV1.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2023 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.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources.NotFoundException;
+import android.media.audiofx.HapticGenerator;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Hosts original Ringtone implementation, retained for flagging large builder+vibration features
+ * in RingtoneV2.java. This does not support new features in the V2 builder.
+ *
+ * Only modified methods are moved here.
+ *
+ * @hide
+ */
+class RingtoneV1 implements Ringtone.ApiInterface {
+ private static final String TAG = "RingtoneV1";
+ 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<RingtoneV1> 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(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private MediaPlayer mLocalPlayer;
+ private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
+ private HapticGenerator mHapticGenerator;
+
+ @UnsupportedAppUsage
+ private Uri mUri;
+ private String mTitle;
+
+ private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ private boolean mPreferBuiltinDevice;
+ // playback properties, use synchronized with mPlaybackSettingsLock
+ private boolean mIsLooping = false;
+ private float mVolume = 1.0f;
+ private boolean mHapticGeneratorEnabled = false;
+ private final Object mPlaybackSettingsLock = new Object();
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ public RingtoneV1(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 {
+ setAudioAttributesField(attributes);
+ // The audio attributes have to be set before the media player is prepared.
+ // Re-initialize it.
+ setUri(mUri, mVolumeShaperConfig);
+ reinitializeActivePlayer();
+ }
+
+ /**
+ * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create
+ * the media player.
+ * @hide
+ */
+ public void setAudioAttributesField(@Nullable AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
+ }
+ mAudioAttributes = attributes;
+ }
+
+ /**
+ * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
+ * the one on which outgoing audio for SIM calls is played.
+ *
+ * @param audioManager the audio manage.
+ * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
+ * none can be found.
+ */
+ private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
+ AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : deviceList) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return device;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the preferred device of the ringtong playback to the built-in device.
+ *
+ * @hide
+ */
+ public boolean preferBuiltinDevice(boolean enable) {
+ mPreferBuiltinDevice = enable;
+ if (mLocalPlayer == null) {
+ return true;
+ }
+ return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager));
+ }
+
+ /**
+ * 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() {
+ Trace.beginSection("reinitializeActivePlayer");
+ if (mUri == null) {
+ Log.e(TAG, "Could not create media player as no URI was provided.");
+ return mAllowRemote && mRemotePlayer != null;
+ }
+ destroyLocalPlayer();
+ // try opening uri locally before delegating to remote player
+ mLocalPlayer = new MediaPlayer();
+ try {
+ mLocalPlayer.setDataSource(mContext, mUri);
+ mLocalPlayer.setAudioAttributes(mAudioAttributes);
+ mLocalPlayer.setPreferredDevice(
+ mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
+ 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");
+ }
+ }
+ Trace.endSection();
+ return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
+ }
+
+ /**
+ * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
+ * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
+ * and if not URI has been set, it will assume no haptic channels are present.
+ * @hide
+ */
+ public boolean hasHapticChannels() {
+ // FIXME: support remote player, or internalize haptic channels support and remove entirely.
+ try {
+ android.os.Trace.beginSection("Ringtone.hasHapticChannels");
+ if (mLocalPlayer != null) {
+ for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
+ if (trackInfo.hasHapticChannels()) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ android.os.Trace.endSection();
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether a local player has been created for this ringtone.
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean hasLocalPlayer() {
+ return mLocalPlayer != null;
+ }
+
+ public @Ringtone.RingtoneMedia int getEnabledMedia() {
+ return Ringtone.MEDIA_SOUND; // RingtoneV2 only
+ }
+
+ public VibrationEffect getVibrationEffect() {
+ return null; // RingtoneV2 only
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * 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) {
+ if (!HapticGenerator.isAvailable()) {
+ return false;
+ }
+ synchronized (mPlaybackSettingsLock) {
+ mHapticGeneratorEnabled = enabled;
+ applyPlaybackProperties_sync();
+ }
+ return true;
+ }
+
+ /**
+ * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
+ * @return true if the HapticGenerator is enabled.
+ */
+ public boolean isHapticGeneratorEnabled() {
+ synchronized (mPlaybackSettingsLock) {
+ return mHapticGeneratorEnabled;
+ }
+ }
+
+ /**
+ * Must be called synchronized on mPlaybackSettingsLock
+ */
+ private void applyPlaybackProperties_sync() {
+ if (mLocalPlayer != null) {
+ mLocalPlayer.setVolume(mVolume);
+ mLocalPlayer.setLooping(mIsLooping);
+ if (mHapticGenerator == null && mHapticGeneratorEnabled) {
+ mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
+ }
+ if (mHapticGenerator != null) {
+ mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
+ try {
+ mRemotePlayer.setPlaybackProperties(
+ mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
+ } 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 = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
+ }
+
+ /**
+ * Set {@link Uri} to be used for ringtone playback.
+ * {@link IRingtonePlayer}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setUri(Uri uri) {
+ setUri(uri, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mVolumeShaperConfig = volumeShaperConfig;
+ }
+
+ /**
+ * 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;
+ mUri = uri;
+ if (mUri == null) {
+ destroyLocalPlayer();
+ }
+ }
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Plays the ringtone.
+ */
+ public void play() {
+ if (mLocalPlayer != null) {
+ // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
+ // (typically because ringer mode is vibrate).
+ if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
+ != 0) {
+ startLocalPlayer();
+ } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
+ // is haptic only ringtone
+ startLocalPlayer();
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != 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) {
+ if (mHapticGenerator != null) {
+ mHapticGenerator.release();
+ mHapticGenerator = 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);
+ }
+ if (LOGD) {
+ Log.d(TAG, "Starting ringtone playback");
+ }
+ 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() {
+ int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
+ if (mAudioManager.getStreamVolume(streamType) == 0) {
+ return false;
+ }
+ int ringtoneType = RingtoneManager.getDefaultType(mUri);
+ if (ringtoneType != -1 &&
+ RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
+ Log.w(TAG, "not playing fallback for " + mUri);
+ return false;
+ }
+ // Default ringtone, try fallback ringtone.
+ try {
+ AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
+ com.android.internal.R.raw.fallbackring);
+ if (afd == null) {
+ Log.e(TAG, "Could not load fallback ringtone");
+ return false;
+ }
+ 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();
+ } catch (IOException ioe) {
+ destroyLocalPlayer();
+ Log.e(TAG, "Failed to open fallback ringtone");
+ return false;
+ } catch (NotFoundException nfe) {
+ Log.e(TAG, "Fallback ringtone does not exist");
+ return false;
+ }
+ return true;
+ }
+
+ public boolean getPreferBuiltinDevice() {
+ return mPreferBuiltinDevice;
+ }
+
+ public VolumeShaper.Configuration getVolumeShaperConfig() {
+ return mVolumeShaperConfig;
+ }
+
+ public boolean isLocalOnly() {
+ return mAllowRemote;
+ }
+
+ public boolean isUsingRemotePlayer() {
+ // V2 testing api, but this is the v1 approximation.
+ return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null);
+ }
+
+ class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.remove(RingtoneV1.this);
+ }
+ mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
+ }
+ }
+}
diff --git a/media/java/android/media/RingtoneV2.java b/media/java/android/media/RingtoneV2.java
new file mode 100644
index 000000000000..f1a81553bdfc
--- /dev/null
+++ b/media/java/android/media/RingtoneV2.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) 2023 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources.NotFoundException;
+import android.media.Ringtone.Injectables;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+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.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * New Ringtone implementation, supporting vibration as well as sound, and configuration via a
+ * builder. During flagged transition, the original implementation is in RingtoneV1.java.
+ *
+ * Only modified methods are moved here.
+ *
+ * @hide
+ */
+class RingtoneV2 implements Ringtone.ApiInterface {
+ private static final String TAG = "RingtoneV2";
+
+ /**
+ * 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')";
+
+ private final Context mContext;
+ private final Vibrator mVibrator;
+ private final AudioManager mAudioManager;
+ private VolumeShaper.Configuration mVolumeShaperConfig;
+
+ /**
+ * Flag indicating if we're allowed to fall back to remote playback using
+ * {@link #mRemoteRingtoneService}. 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 mRemoteRingtoneService;
+ private final Injectables mInjectables;
+
+ private final int mEnabledMedia;
+
+ private final Uri mUri;
+ private String mTitle;
+
+ private AudioAttributes mAudioAttributes;
+ private boolean mUseExactAudioAttributes;
+ private boolean mPreferBuiltinDevice;
+ private RingtonePlayer mActivePlayer;
+ // playback properties, use synchronized with mPlaybackSettingsLock
+ private boolean mIsLooping;
+ private float mVolume;
+ private boolean mHapticGeneratorEnabled;
+ private final Object mPlaybackSettingsLock = new Object();
+ private final VibrationEffect mVibrationEffect;
+
+ /** Only for use by Ringtone constructor */
+ RingtoneV2(@NonNull Context context, @NonNull Injectables injectables,
+ boolean allowRemote, @Ringtone.RingtoneMedia int enabledMedia,
+ @Nullable Uri uri, @NonNull AudioAttributes audioAttributes,
+ boolean useExactAudioAttributes,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean preferBuiltinDevice, float soundVolume, boolean looping,
+ boolean hapticGeneratorEnabled, @Nullable VibrationEffect vibrationEffect) {
+ // Context
+ mContext = context;
+ mInjectables = injectables;
+ mVibrator = mContext.getSystemService(Vibrator.class);
+ mAudioManager = mContext.getSystemService(AudioManager.class);
+ mRemoteRingtoneService = allowRemote ? mAudioManager.getRingtonePlayer() : null;
+ mAllowRemote = (mRemoteRingtoneService != null); // Only set if allowed, and present.
+
+ // Properties potentially propagated to remote player.
+ mEnabledMedia = enabledMedia;
+ mUri = uri;
+ mAudioAttributes = audioAttributes;
+ mUseExactAudioAttributes = useExactAudioAttributes;
+ mVolumeShaperConfig = volumeShaperConfig;
+ mPreferBuiltinDevice = preferBuiltinDevice; // system-only, not supported for remote play.
+ mVolume = soundVolume;
+ mIsLooping = looping;
+ mHapticGeneratorEnabled = hapticGeneratorEnabled;
+ mVibrationEffect = vibrationEffect;
+ }
+
+ /** @hide */
+ @RingtoneMedia
+ public int getEnabledMedia() {
+ return mEnabledMedia;
+ }
+
+ /**
+ * 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) {
+ setAudioAttributes(
+ getAudioAttributesForLegacyStreamType(streamType, "setStreamType()"));
+ }
+
+ private AudioAttributes getAudioAttributesForLegacyStreamType(int streamType, String originOp) {
+ PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", originOp);
+ return 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 {
+ // TODO: deprecate this method - it will be done with a builder.
+ if (attributes == null) {
+ throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
+ }
+ mAudioAttributes = attributes;
+ // Setting the audio attributes requires re-initializing the player.
+ if (mActivePlayer != null) {
+ // The audio attributes have to be set before the media player is prepared.
+ // Re-initialize it.
+ reinitializeActivePlayer();
+ }
+ }
+
+ /**
+ * Returns the vibration effect that this ringtone was created with, if vibration is enabled.
+ * Otherwise, returns null.
+ * @hide
+ */
+ @Nullable
+ public VibrationEffect getVibrationEffect() {
+ return mVibrationEffect;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public boolean getPreferBuiltinDevice() {
+ return mPreferBuiltinDevice;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public VolumeShaper.Configuration getVolumeShaperConfig() {
+ return mVolumeShaperConfig;
+ }
+
+ /**
+ * 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 !mAllowRemote;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public boolean isUsingRemotePlayer() {
+ return mActivePlayer instanceof RemoteRingtonePlayer;
+ }
+
+ /**
+ * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
+ * the one on which outgoing audio for SIM calls is played.
+ *
+ * @param audioManager the audio manage.
+ * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
+ * none can be found.
+ */
+ private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
+ AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : deviceList) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return device;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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() {
+ // Try creating a local media player, or fallback to creating a remote one.
+ Trace.beginSection("reinitializeActivePlayer");
+ try {
+ if (mActivePlayer != null) {
+ // This would only happen if calling the deprecated setAudioAttributes after
+ // building the Ringtone.
+ stopAndReleaseActivePlayer();
+ }
+
+ boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION;
+ // Vibration can come from the audio file if using haptic generator or if haptic
+ // channels are a possibility.
+ boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported()
+ && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted());
+
+ // VibrationEffect only, use the simplified player without checking for haptic channels.
+ if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) {
+ mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer(
+ mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping);
+ return true;
+ }
+
+ AudioDeviceInfo preferredDevice =
+ mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
+ if (mUri != null) {
+ mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri,
+ mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables,
+ mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping,
+ mVolume);
+ } else {
+ // Using the remote player won't help play a null Uri. Revert straight to fallback.
+ // The vibration-only case was already covered above.
+ mActivePlayer = createFallbackRingtonePlayer();
+ // Fall through to attempting remote fallback play if null.
+ }
+
+ if (mActivePlayer == null && mAllowRemote) {
+ mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri,
+ mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
+ mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume);
+ }
+
+ return mActivePlayer != null;
+ } finally {
+ if (mActivePlayer != null) {
+ Log.d(TAG, "Initialized ringtone player with " + mActivePlayer.getClass());
+ } else {
+ Log.d(TAG, "Failed to initialize ringtone player");
+ }
+ Trace.endSection();
+ }
+ }
+
+ @Nullable
+ private LocalRingtonePlayer createFallbackRingtonePlayer() {
+ int ringtoneType = RingtoneManager.getDefaultType(mUri);
+ if (ringtoneType != -1
+ && RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
+ Log.w(TAG, "not playing fallback for " + mUri);
+ return null;
+ }
+ // Default ringtone, try fallback ringtone.
+ try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
+ com.android.internal.R.raw.fallbackring)) {
+ if (afd == null) {
+ Log.e(TAG, "Could not load fallback ringtone");
+ return null;
+ }
+
+ AudioDeviceInfo preferredDevice =
+ mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
+ return LocalRingtonePlayer.createForFallback(mAudioManager, mVibrator, afd,
+ mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig,
+ preferredDevice, mIsLooping, mVolume);
+ } catch (NotFoundException nfe) {
+ Log.e(TAG, "Fallback ringtone does not exist");
+ return null;
+ } catch (IOException e) {
+ // As with the above messages, not including much information about the
+ // failure so as not to expose details of the fallback ringtone resource.
+ Log.e(TAG, "Exception reading fallback ringtone");
+ return null;
+ }
+ }
+
+ /**
+ * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
+ * @hide
+ */
+ public boolean hasHapticChannels() {
+ return (mActivePlayer == null) ? false : mActivePlayer.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 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;
+ if (mActivePlayer != null) {
+ mActivePlayer.setLooping(looping);
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ // Ignore if sound not enabled.
+ if ((mEnabledMedia & MEDIA_SOUND) == 0) {
+ return;
+ }
+ if (volume < 0.0f) {
+ volume = 0.0f;
+ } else if (volume > 1.0f) {
+ volume = 1.0f;
+ }
+
+ synchronized (mPlaybackSettingsLock) {
+ mVolume = volume;
+ if (mActivePlayer != null) {
+ mActivePlayer.setVolume(volume);
+ }
+ }
+ }
+
+ /**
+ * Returns the volume scalar set on this player.
+ * @return a value between 0.0f and 1.0f.
+ */
+ public float getVolume() {
+ synchronized (mPlaybackSettingsLock) {
+ return mVolume;
+ }
+ }
+
+ /**
+ * 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) {
+ if (!mInjectables.isHapticGeneratorAvailable()) {
+ return false;
+ }
+ synchronized (mPlaybackSettingsLock) {
+ mHapticGeneratorEnabled = enabled;
+ if (mActivePlayer != null) {
+ mActivePlayer.setHapticGeneratorEnabled(enabled);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
+ * @return true if the HapticGenerator is enabled.
+ */
+ public boolean isHapticGeneratorEnabled() {
+ synchronized (mPlaybackSettingsLock) {
+ return mHapticGeneratorEnabled;
+ }
+ }
+
+ /**
+ * 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 = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
+ }
+
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Plays the ringtone.
+ */
+ public void play() {
+ if (mActivePlayer != null) {
+ Log.d(TAG, "Starting ringtone playback");
+ if (mActivePlayer.play()) {
+ return;
+ } else {
+ // Discard active player: play() is only meant to be called once.
+ stopAndReleaseActivePlayer();
+ }
+ }
+ if (!playFallbackRingtone()) {
+ Log.w(TAG, "Neither local nor remote playback available");
+ }
+ }
+
+ /**
+ * Stops a playing ringtone.
+ */
+ public void stop() {
+ stopAndReleaseActivePlayer();
+ }
+
+ private void stopAndReleaseActivePlayer() {
+ if (mActivePlayer != null) {
+ mActivePlayer.stopAndRelease();
+ mActivePlayer = null;
+ }
+ }
+
+ /**
+ * Whether this ringtone is currently playing.
+ *
+ * @return True if playing, false otherwise.
+ */
+ public boolean isPlaying() {
+ if (mActivePlayer != null) {
+ return mActivePlayer.isPlaying();
+ } else {
+ Log.w(TAG, "No active ringtone player");
+ return false;
+ }
+ }
+
+ /**
+ * Fallback during the play stage rather than initialization, typically due to an issue
+ * communicating with the remote player.
+ */
+ private boolean playFallbackRingtone() {
+ if (mActivePlayer != null) {
+ Log.wtf(TAG, "Playing fallback ringtone with another active player");
+ stopAndReleaseActivePlayer();
+ }
+ int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
+ if (mAudioManager.getStreamVolume(streamType) == 0) {
+ // TODO: Return true? If volume is off, this is a successful play.
+ return false;
+ }
+ mActivePlayer = createFallbackRingtonePlayer();
+ if (mActivePlayer == null) {
+ return false; // the create method logs if it returns null.
+ } else if (mActivePlayer.play()) {
+ return true;
+ } else {
+ stopAndReleaseActivePlayer();
+ return false;
+ }
+ }
+
+ void setTitle(String title) {
+ mTitle = title;
+ }
+
+ /**
+ * Play a specific ringtone. This interface is implemented by either local (this process) or
+ * proxied-remote playback via AudioManager.getRingtonePlayer, so that the caller
+ * (Ringtone class) can just use a single player after the initial creation.
+ * @hide
+ */
+ interface RingtonePlayer {
+ /**
+ * Start playing the ringtone, returning false if there was a problem that
+ * requires falling back to the fallback ringtone resource.
+ */
+ boolean play();
+ boolean isPlaying();
+ void stopAndRelease();
+
+ // Mutating playback methods.
+ void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo);
+ void setLooping(boolean looping);
+ void setHapticGeneratorEnabled(boolean enabled);
+ void setVolume(float volume);
+
+ boolean hasHapticChannels();
+ }
+
+ /**
+ * Remote RingtonePlayer. All operations are delegated via the IRingtonePlayer interface, which
+ * should ultimately be backed by a RingtoneLocalPlayer within the system services.
+ */
+ static class RemoteRingtonePlayer implements RingtonePlayer {
+ private final IBinder mRemoteToken = new Binder();
+ private final IRingtonePlayer mRemoteRingtoneService;
+ private final Uri mCanonicalUri;
+ private final int mEnabledMedia;
+ private final VibrationEffect mVibrationEffect;
+ private final VolumeShaper.Configuration mVolumeShaperConfig;
+ private final AudioAttributes mAudioAttributes;
+ private final boolean mUseExactAudioAttributes;
+ private boolean mIsLooping;
+ private float mVolume;
+ private boolean mHapticGeneratorEnabled;
+
+ RemoteRingtonePlayer(@NonNull IRingtonePlayer remoteRingtoneService,
+ @NonNull Uri uri, @NonNull AudioAttributes audioAttributes,
+ boolean useExactAudioAttributes,
+ @RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
+ mRemoteRingtoneService = remoteRingtoneService;
+ mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri();
+ mAudioAttributes = audioAttributes;
+ mUseExactAudioAttributes = useExactAudioAttributes;
+ mEnabledMedia = enabledMedia;
+ mVibrationEffect = vibrationEffect;
+ mVolumeShaperConfig = volumeShaperConfig;
+ mHapticGeneratorEnabled = hapticGeneratorEnabled;
+ mIsLooping = initialIsLooping;
+ mVolume = initialVolume;
+ }
+
+ @Override
+ public boolean play() {
+ try {
+ mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri,
+ mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
+ mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig);
+ return true;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem playing ringtone: " + e);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isPlaying() {
+ try {
+ return mRemoteRingtoneService.isPlaying(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem checking ringtone isPlaying: " + e);
+ return false;
+ }
+ }
+
+ @Override
+ public void stopAndRelease() {
+ try {
+ mRemoteRingtoneService.stop(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem stopping ringtone: " + e);
+ }
+ }
+
+ @Override
+ public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
+ // un-implemented for remote (but not used outside system).
+ }
+
+ @Override
+ public void setLooping(boolean looping) {
+ mIsLooping = looping;
+ try {
+ mRemoteRingtoneService.setLooping(mRemoteToken, looping);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem setting looping: " + e);
+ }
+ }
+
+ @Override
+ public void setHapticGeneratorEnabled(boolean enabled) {
+ mHapticGeneratorEnabled = enabled;
+ try {
+ mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem setting hapticGeneratorEnabled: " + e);
+ }
+ }
+
+ @Override
+ public void setVolume(float volume) {
+ mVolume = volume;
+ try {
+ mRemoteRingtoneService.setVolume(mRemoteToken, volume);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem setting volume: " + e);
+ }
+ }
+
+ @Override
+ public boolean hasHapticChannels() {
+ // FIXME: support remote player, or internalize haptic channels support and remove
+ // entirely.
+ return false;
+ }
+ }
+
+}
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java
index cb41eabfa87a..6817f534c00b 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java
@@ -19,6 +19,7 @@ package com.android.soundpicker;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.Ringtone;
+import android.media.RingtoneManager;
import android.net.Uri;
import dagger.hilt.android.qualifiers.ApplicationContext;
@@ -53,10 +54,7 @@ public class RingtoneFactory {
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setFlags(audioAttributesFlags)
.build();
- // TODO: We are currently only using MEDIA_SOUND for enabledMedia. This will change once we
- // start playing sound and/or vibration.
- return new Ringtone.Builder(mApplicationContext, Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(uri)
- .build();
+ return RingtoneManager.getRingtone(mApplicationContext, uri,
+ /* volumeShaperConfig= */ null, audioAttributes);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 6d9844d9ec59..68202d5629a0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -57,7 +57,7 @@ import javax.inject.Inject;
@SysUISingleton
public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
- private static final boolean LOGD = false;
+ private static final boolean LOGD = true;
private final Context mContext;
// TODO: support Uri switching under same IBinder
@@ -111,9 +111,53 @@ public class RingtonePlayer implements CoreStartable {
@Override
public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
throws RemoteException {
- playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
- null, volume, looping, /* hapticGenerator= */ false,
- null);
+ if (Ringtone.useRingtoneV2()) {
+ playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
+ null, volume, looping, /* hapticGenerator= */ false,
+ null);
+ } else {
+ playWithVolumeShaping(token, uri, aa, volume, looping, null);
+ }
+ }
+
+ @Override
+ public void playWithVolumeShaping(
+ IBinder token, Uri uri, AudioAttributes aa, float volume,
+ boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig)
+ throws RemoteException {
+ if (LOGD) {
+ Log.d(TAG, "playWithVolumeShaping(token=" + token + ", uri=" + uri + ", uid="
+ + Binder.getCallingUid() + ")");
+ }
+ Client client;
+ synchronized (mClients) {
+ client = mClients.get(token);
+ }
+ // Don't hold the lock while constructing the ringtone, since it can be slow. The caller
+ // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
+ // waste the build.
+ if (client == null) {
+ final UserHandle user = Binder.getCallingUserHandle();
+ Ringtone ringtone = Ringtone.createV1WithCustomAudioAttributes(
+ getContextForUser(user), aa, uri, volumeShaperConfig,
+ /* allowRemote= */ false);
+ synchronized (mClients) {
+ client = mClients.get(token);
+ if (client == null) {
+ client = new Client(token, ringtone);
+ token.linkToDeath(client, 0);
+ mClients.put(token, client);
+ ringtone = null; // "owned" by the client now.
+ }
+ }
+ // Clean up ringtone if it was abandoned (a client already existed).
+ if (ringtone != null) {
+ ringtone.stop();
+ }
+ }
+ client.mRingtone.setLooping(looping);
+ client.mRingtone.setVolume(volume);
+ client.mRingtone.play();
}
@Override
@@ -125,7 +169,7 @@ public class RingtonePlayer implements CoreStartable {
@Nullable VolumeShaper.Configuration volumeShaperConfig)
throws RemoteException {
if (LOGD) {
- Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Log.d(TAG, "playRemoteRingtone(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
@@ -190,6 +234,21 @@ public class RingtonePlayer implements CoreStartable {
return false;
}
}
+ @Override
+ public void setPlaybackProperties(IBinder token, float volume, boolean looping,
+ boolean hapticGeneratorEnabled) {
+ // RingtoneV1-exclusive path.
+ Client client;
+ synchronized (mClients) {
+ client = mClients.get(token);
+ }
+ if (client != null) {
+ client.mRingtone.setVolume(volume);
+ client.mRingtone.setLooping(looping);
+ client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled);
+ }
+ // else no client for token when setting playback properties but will be set at play()
+ }
@Override
public void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) {