diff options
author | 2023-07-31 14:35:32 +0000 | |
---|---|---|
committer | 2023-08-02 10:42:04 +0000 | |
commit | 549920720ffc7b29adc13041492d6116fa391077 (patch) | |
tree | 02fed6d5aa741e8e5c46bc7c16eb344bab666bcd | |
parent | d3f21133a08dc21d949f676517235ef5a86bd369 (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.aidl | 12 | ||||
-rw-r--r-- | media/java/android/media/LocalRingtonePlayer.java | 10 | ||||
-rw-r--r-- | media/java/android/media/Ringtone.java | 527 | ||||
-rw-r--r-- | media/java/android/media/RingtoneManager.java | 59 | ||||
-rw-r--r-- | media/java/android/media/RingtoneV1.java | 614 | ||||
-rw-r--r-- | media/java/android/media/RingtoneV2.java | 690 | ||||
-rw-r--r-- | packages/SoundPicker/src/com/android/soundpicker/RingtoneFactory.java | 8 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java | 69 |
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) { |