Add support for VibrationEffect and audio-coupled vibrations to Ringtone.
Rename playWithVolumeShaping to playRemoteRingtone, to make the application clearer. Bug: 272280617, 279157059 Test: presubmit Change-Id: Ic1b753cde9afc6536d137618df54c21eec38487a
This commit is contained in:
@@ -34,6 +34,7 @@ import android.os.vibrator.PrimitiveSegment;
|
||||
import android.os.vibrator.RampSegment;
|
||||
import android.os.vibrator.StepSegment;
|
||||
import android.os.vibrator.VibrationEffectSegment;
|
||||
import android.util.Log;
|
||||
import android.util.MathUtils;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
@@ -52,6 +53,7 @@ import java.util.Objects;
|
||||
* <p>These effects may be any number of things, from single shot vibrations to complex waveforms.
|
||||
*/
|
||||
public abstract class VibrationEffect implements Parcelable {
|
||||
private static final String TAG = "VibrationEffect";
|
||||
// Stevens' coefficient to scale the perceived vibration intensity.
|
||||
private static final float SCALE_GAMMA = 0.65f;
|
||||
// If a vibration is playing for longer than 1s, it's probably not haptic feedback
|
||||
@@ -394,26 +396,32 @@ public abstract class VibrationEffect implements Parcelable {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
Uri uncanonicalUri = cr.uncanonicalize(uri);
|
||||
if (uncanonicalUri == null) {
|
||||
// If we already had an uncanonical URI, it's possible we'll get null back here. In
|
||||
// this case, just use the URI as passed in since it wasn't canonicalized in the first
|
||||
// place.
|
||||
uncanonicalUri = uri;
|
||||
}
|
||||
try {
|
||||
final ContentResolver cr = context.getContentResolver();
|
||||
Uri uncanonicalUri = cr.uncanonicalize(uri);
|
||||
if (uncanonicalUri == null) {
|
||||
// If we already had an uncanonical URI, it's possible we'll get null back here. In
|
||||
// this case, just use the URI as passed in since it wasn't canonicalized in the
|
||||
// first place.
|
||||
uncanonicalUri = uri;
|
||||
}
|
||||
|
||||
for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
|
||||
if (uris[i] == null) {
|
||||
continue;
|
||||
}
|
||||
Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
|
||||
if (mappedUri == null) {
|
||||
continue;
|
||||
}
|
||||
if (mappedUri.equals(uncanonicalUri)) {
|
||||
return get(RINGTONES[i]);
|
||||
for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
|
||||
if (uris[i] == null) {
|
||||
continue;
|
||||
}
|
||||
Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
|
||||
if (mappedUri == null) {
|
||||
continue;
|
||||
}
|
||||
if (mappedUri.equals(uncanonicalUri)) {
|
||||
return get(RINGTONES[i]);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Don't give unexpected exceptions to callers if the Uri's ContentProvider is
|
||||
// misbehaving - it's very unlikely to be mapped in that case anyway.
|
||||
Log.e(TAG, "Exception getting default vibration for Uri " + uri, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.media.VolumeShaper;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.UserHandle;
|
||||
import android.os.VibrationEffect;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
@@ -29,7 +30,8 @@ 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 playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
|
||||
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);
|
||||
|
||||
@@ -23,6 +23,9 @@ import android.content.res.AssetFileDescriptor;
|
||||
import android.media.audiofx.HapticGenerator;
|
||||
import android.net.Uri;
|
||||
import android.os.Trace;
|
||||
import android.os.VibrationAttributes;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -36,21 +39,27 @@ import java.util.Objects;
|
||||
public class LocalRingtonePlayer
|
||||
implements Ringtone.RingtonePlayer, MediaPlayer.OnCompletionListener {
|
||||
private static final String TAG = "LocalRingtonePlayer";
|
||||
private static final int VIBRATION_LOOP_DELAY_MS = 200;
|
||||
|
||||
// keep references on active Ringtones until stopped or completion listener called.
|
||||
private static final ArrayList<LocalRingtonePlayer> sActiveRingtones = new ArrayList<>();
|
||||
private static final ArrayList<LocalRingtonePlayer> sActiveMediaPlayers = new ArrayList<>();
|
||||
|
||||
private final MediaPlayer mMediaPlayer;
|
||||
private final AudioAttributes mAudioAttributes;
|
||||
private final VibrationAttributes mVibrationAttributes;
|
||||
private final Ringtone.Injectables mInjectables;
|
||||
private final AudioManager mAudioManager;
|
||||
private final VolumeShaper mVolumeShaper;
|
||||
private final Vibrator mVibrator;
|
||||
private final VibrationEffect mVibrationEffect;
|
||||
private HapticGenerator mHapticGenerator;
|
||||
private boolean mStartedVibration;
|
||||
|
||||
private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer,
|
||||
@NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables,
|
||||
@NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator,
|
||||
@Nullable VolumeShaper volumeShaper) {
|
||||
@Nullable VolumeShaper volumeShaper, @NonNull Vibrator vibrator,
|
||||
@Nullable VibrationEffect vibrationEffect) {
|
||||
Objects.requireNonNull(mediaPlayer);
|
||||
Objects.requireNonNull(audioAttributes);
|
||||
Objects.requireNonNull(injectables);
|
||||
@@ -60,7 +69,11 @@ public class LocalRingtonePlayer
|
||||
mInjectables = injectables;
|
||||
mAudioManager = audioManager;
|
||||
mVolumeShaper = volumeShaper;
|
||||
mVibrator = vibrator;
|
||||
mVibrationEffect = vibrationEffect;
|
||||
mHapticGenerator = hapticGenerator;
|
||||
mVibrationAttributes = (mVibrationEffect == null) ? null :
|
||||
new VibrationAttributes.Builder(audioAttributes).build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +82,9 @@ public class LocalRingtonePlayer
|
||||
*/
|
||||
@Nullable
|
||||
static LocalRingtonePlayer create(@NonNull Context context,
|
||||
@NonNull AudioManager audioManager, @NonNull Uri soundUri,
|
||||
@NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri,
|
||||
@NonNull AudioAttributes audioAttributes,
|
||||
@Nullable VibrationEffect vibrationEffect,
|
||||
@NonNull Ringtone.Injectables injectables,
|
||||
@Nullable VolumeShaper.Configuration volumeShaperConfig,
|
||||
@Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled,
|
||||
@@ -89,15 +103,26 @@ public class LocalRingtonePlayer
|
||||
mediaPlayer.setVolume(initialVolume);
|
||||
if (initialHapticGeneratorEnabled) {
|
||||
hapticGenerator = injectables.createHapticGenerator(mediaPlayer);
|
||||
hapticGenerator.setEnabled(true);
|
||||
if (hapticGenerator != null) {
|
||||
// In practise, this should always be non-null because the initial value is
|
||||
// not true unless it's available.
|
||||
hapticGenerator.setEnabled(true);
|
||||
vibrationEffect = null; // Don't play the VibrationEffect.
|
||||
}
|
||||
}
|
||||
VolumeShaper volumeShaper = null;
|
||||
if (volumeShaperConfig != null) {
|
||||
volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
|
||||
}
|
||||
mediaPlayer.prepare();
|
||||
if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
|
||||
if (injectables.hasHapticChannels(mediaPlayer)) {
|
||||
// Don't play the Vibration effect if the URI has haptic channels.
|
||||
vibrationEffect = null;
|
||||
}
|
||||
}
|
||||
return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
|
||||
hapticGenerator, volumeShaper);
|
||||
hapticGenerator, volumeShaper, vibrator, vibrationEffect);
|
||||
} catch (SecurityException | IOException e) {
|
||||
if (hapticGenerator != null) {
|
||||
hapticGenerator.release();
|
||||
@@ -116,8 +141,10 @@ public class LocalRingtonePlayer
|
||||
*/
|
||||
@Nullable
|
||||
static LocalRingtonePlayer createForFallback(
|
||||
@NonNull AudioManager audioManager, @NonNull AssetFileDescriptor afd,
|
||||
@NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
|
||||
@NonNull AssetFileDescriptor afd,
|
||||
@NonNull AudioAttributes audioAttributes,
|
||||
@Nullable VibrationEffect vibrationEffect,
|
||||
@NonNull Ringtone.Injectables injectables,
|
||||
@Nullable VolumeShaper.Configuration volumeShaperConfig,
|
||||
@Nullable AudioDeviceInfo preferredDevice,
|
||||
@@ -146,10 +173,17 @@ public class LocalRingtonePlayer
|
||||
volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
|
||||
}
|
||||
mediaPlayer.prepare();
|
||||
return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
|
||||
/* hapticGenerator= */ null, volumeShaper);
|
||||
if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
|
||||
if (injectables.hasHapticChannels(mediaPlayer)) {
|
||||
// Don't play the Vibration effect if the URI has haptic channels.
|
||||
vibrationEffect = null;
|
||||
}
|
||||
}
|
||||
return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
|
||||
/* hapticGenerator= */ null, volumeShaper, vibrator, vibrationEffect);
|
||||
} catch (SecurityException | IOException e) {
|
||||
Log.e(TAG, "Failed to open fallback ringtone");
|
||||
// TODO: vibration-effect-only / no-sound LocalRingtonePlayer.
|
||||
mediaPlayer.release();
|
||||
return null;
|
||||
} finally {
|
||||
@@ -163,10 +197,14 @@ public class LocalRingtonePlayer
|
||||
// (typically because ringer mode is vibrate).
|
||||
if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
|
||||
== 0 && (mAudioAttributes.areHapticChannelsMuted() || !hasHapticChannels())) {
|
||||
maybeStartVibration();
|
||||
return true; // Successfully played while muted.
|
||||
}
|
||||
synchronized (sActiveRingtones) {
|
||||
sActiveRingtones.add(this);
|
||||
synchronized (sActiveMediaPlayers) {
|
||||
// We keep-alive when a mediaplayer is active, since its finalizer would stop the
|
||||
// ringtone. This isn't necessary for vibrations in the vibrator service
|
||||
// (i.e. maybeStartVibration in the muted case, above).
|
||||
sActiveMediaPlayers.add(this);
|
||||
}
|
||||
|
||||
mMediaPlayer.setOnCompletionListener(this);
|
||||
@@ -174,9 +212,41 @@ public class LocalRingtonePlayer
|
||||
if (mVolumeShaper != null) {
|
||||
mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
|
||||
}
|
||||
maybeStartVibration();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void maybeStartVibration() {
|
||||
if (mVibrationEffect != null && !mStartedVibration) {
|
||||
boolean isLooping = mMediaPlayer.isLooping();
|
||||
try {
|
||||
// Adjust the vibration effect to loop.
|
||||
VibrationEffect loopAdjustedEffect = mVibrationEffect.applyRepeatingIndefinitely(
|
||||
isLooping, VIBRATION_LOOP_DELAY_MS);
|
||||
mVibrator.vibrate(loopAdjustedEffect, mVibrationAttributes);
|
||||
mStartedVibration = true;
|
||||
} catch (Exception e) {
|
||||
// Catch exceptions widely, because we don't want to "leak" looping sounds or
|
||||
// vibrations if something goes wrong.
|
||||
Log.e(TAG, "Problem starting " + (isLooping ? "looping " : "") + "vibration "
|
||||
+ "for ringtone: " + mVibrationEffect, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopVibration() {
|
||||
if (mVibrationEffect != null && mStartedVibration) {
|
||||
try {
|
||||
mVibrator.cancel(mVibrationAttributes.getUsage());
|
||||
mStartedVibration = false;
|
||||
} catch (Exception e) {
|
||||
// Catch exceptions widely, because we don't want to "leak" looping sounds or
|
||||
// vibrations if something goes wrong.
|
||||
Log.e(TAG, "Problem stopping vibration for ringtone", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mMediaPlayer.isPlaying();
|
||||
@@ -184,15 +254,20 @@ public class LocalRingtonePlayer
|
||||
|
||||
@Override
|
||||
public void stopAndRelease() {
|
||||
synchronized (sActiveRingtones) {
|
||||
sActiveRingtones.remove(this);
|
||||
synchronized (sActiveMediaPlayers) {
|
||||
sActiveMediaPlayers.remove(this);
|
||||
}
|
||||
if (mHapticGenerator != null) {
|
||||
mHapticGenerator.release();
|
||||
try {
|
||||
mMediaPlayer.stop();
|
||||
} finally {
|
||||
stopVibration(); // Won't throw: catches exceptions.
|
||||
if (mHapticGenerator != null) {
|
||||
mHapticGenerator.release();
|
||||
}
|
||||
mMediaPlayer.setOnCompletionListener(null);
|
||||
mMediaPlayer.reset();
|
||||
mMediaPlayer.release();
|
||||
}
|
||||
mMediaPlayer.setOnCompletionListener(null);
|
||||
mMediaPlayer.reset();
|
||||
mMediaPlayer.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,11 +277,29 @@ public class LocalRingtonePlayer
|
||||
|
||||
@Override
|
||||
public void setLooping(boolean looping) {
|
||||
boolean wasLooping = mMediaPlayer.isLooping();
|
||||
if (wasLooping == looping) {
|
||||
return;
|
||||
}
|
||||
mMediaPlayer.setLooping(looping);
|
||||
// If transitioning from looping to not-looping during play, then cancel the vibration.
|
||||
if (mVibrationEffect != null && mMediaPlayer.isPlaying()) {
|
||||
if (wasLooping) {
|
||||
stopVibration();
|
||||
} else {
|
||||
// Won't restart the vibration to be looping if it was already started.
|
||||
maybeStartVibration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHapticGeneratorEnabled(boolean enabled) {
|
||||
if (mVibrationEffect != null) {
|
||||
// Ignore haptic generator changes if a vibration effect is present. The decision to
|
||||
// use one or the other happens before this object is constructed.
|
||||
return;
|
||||
}
|
||||
if (enabled && mHapticGenerator == null) {
|
||||
mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer);
|
||||
}
|
||||
@@ -220,30 +313,15 @@ public class LocalRingtonePlayer
|
||||
mMediaPlayer.setVolume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public boolean hasHapticChannels() {
|
||||
// FIXME: support remote player, or internalize haptic channels support and remove entirely.
|
||||
try {
|
||||
Trace.beginSection("LocalRingtonePlayer.hasHapticChannels");
|
||||
for (MediaPlayer.TrackInfo trackInfo : mMediaPlayer.getTrackInfo()) {
|
||||
if (trackInfo.hasHapticChannels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
return false;
|
||||
return mInjectables.hasHapticChannels(mMediaPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
synchronized (sActiveRingtones) {
|
||||
sActiveRingtones.remove(this);
|
||||
synchronized (sActiveMediaPlayers) {
|
||||
sActiveMediaPlayers.remove(this);
|
||||
}
|
||||
mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
@@ -23,6 +24,7 @@ import android.compat.annotation.UnsupportedAppUsage;
|
||||
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;
|
||||
@@ -32,6 +34,8 @@ 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.provider.Settings;
|
||||
@@ -62,6 +66,7 @@ public class Ringtone {
|
||||
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;
|
||||
@@ -96,6 +101,7 @@ public class Ringtone {
|
||||
+ 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;
|
||||
|
||||
@@ -114,27 +120,42 @@ public class Ringtone {
|
||||
private String mTitle;
|
||||
|
||||
private AudioAttributes mAudioAttributes;
|
||||
private boolean mUseExactAudioAttributes;
|
||||
private boolean mPreferBuiltinDevice;
|
||||
private RingtonePlayer mActivePlayer;
|
||||
// playback properties, use synchronized with mPlaybackSettingsLock
|
||||
private boolean mIsLooping = false;
|
||||
private boolean mIsLooping;
|
||||
private float mVolume;
|
||||
private boolean mHapticGeneratorEnabled = false;
|
||||
private boolean mHapticGeneratorEnabled;
|
||||
private final Object mPlaybackSettingsLock = new Object();
|
||||
private final VibrationEffect mVibrationEffect;
|
||||
|
||||
private Ringtone(Builder builder) {
|
||||
private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia,
|
||||
@NonNull AudioAttributes effectiveAudioAttributes,
|
||||
@Nullable VibrationEffect effectiveVibrationEffect,
|
||||
boolean effectiveHapticGeneratorEnabled) {
|
||||
// Context
|
||||
mContext = builder.mContext;
|
||||
mEnabledMedia = builder.mEnabledMedia;
|
||||
mUri = builder.mUri;
|
||||
mAudioAttributes = builder.mAudioAttributes;
|
||||
mInjectables = builder.mInjectables;
|
||||
mPreferBuiltinDevice = builder.mPreferBuiltinDevice;
|
||||
//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;
|
||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
mRemoteRingtoneService = builder.mAllowRemote ? mAudioManager.getRingtonePlayer() : null;
|
||||
mAllowRemote = (mRemoteRingtoneService != null);
|
||||
mVibrationEffect = effectiveVibrationEffect;
|
||||
mAudioAttributes = effectiveAudioAttributes;
|
||||
mUseExactAudioAttributes = builder.mUseExactAudioAttributes;
|
||||
mHapticGeneratorEnabled = effectiveHapticGeneratorEnabled;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@@ -193,6 +214,16 @@ public class Ringtone {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
@@ -258,9 +289,9 @@ public class Ringtone {
|
||||
AudioDeviceInfo preferredDevice =
|
||||
mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
|
||||
if (mUri != null) {
|
||||
mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mUri,
|
||||
mAudioAttributes, mInjectables, mVolumeShaperConfig, preferredDevice,
|
||||
mHapticGeneratorEnabled, mIsLooping, mVolume);
|
||||
mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri,
|
||||
mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig,
|
||||
preferredDevice, mHapticGeneratorEnabled, mIsLooping, mVolume);
|
||||
} else {
|
||||
// Using the remote player won't help play a null Uri. Revert straight to fallback.
|
||||
mActivePlayer = createFallbackRingtonePlayer();
|
||||
@@ -269,9 +300,10 @@ public class Ringtone {
|
||||
|
||||
if (mActivePlayer == null && mAllowRemote) {
|
||||
mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri,
|
||||
mAudioAttributes,
|
||||
mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
|
||||
mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume);
|
||||
}
|
||||
|
||||
return mActivePlayer != null;
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
@@ -296,9 +328,9 @@ public class Ringtone {
|
||||
|
||||
AudioDeviceInfo preferredDevice =
|
||||
mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
|
||||
return LocalRingtonePlayer.createForFallback(mAudioManager, afd,
|
||||
mAudioAttributes, mInjectables, mVolumeShaperConfig, preferredDevice,
|
||||
mIsLooping, mVolume);
|
||||
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;
|
||||
@@ -575,7 +607,9 @@ public class Ringtone {
|
||||
/**
|
||||
* Build a {@link Ringtone} to easily play sounds for ringtones, alarms and notifications.
|
||||
*
|
||||
* TODO: when un-hide, deprecate Ringtone: setAudioAttributes.
|
||||
* TODO: when un-hide, deprecate Ringtone: setAudioAttributes, setLooping,
|
||||
* setHapticGeneratorEnabled (no-effect if MEDIA_VIBRATION),
|
||||
* static RingtoneManager.getRingtone.
|
||||
* @hide
|
||||
*/
|
||||
public static final class Builder {
|
||||
@@ -583,13 +617,16 @@ public class Ringtone {
|
||||
private final int mEnabledMedia;
|
||||
private Uri mUri;
|
||||
private final AudioAttributes mAudioAttributes;
|
||||
private boolean mUseExactAudioAttributes = false;
|
||||
// Not a static default since it doesn't really need to be in memory forever.
|
||||
private Injectables mInjectables = new Injectables();
|
||||
private VolumeShaper.Configuration mVolumeShaperConfig;
|
||||
private boolean mPreferBuiltinDevice = false;
|
||||
private boolean mAllowRemote = true;
|
||||
private boolean mHapticGeneratorEnabled = false;
|
||||
private float mInitialSoundVolume = 1.0f;
|
||||
private boolean mLooping = false;
|
||||
private VibrationEffect mVibrationEffect;
|
||||
|
||||
/**
|
||||
* Constructs a builder to play the given media types from the mediaUri. If the mediaUri
|
||||
@@ -599,15 +636,14 @@ public class Ringtone {
|
||||
* silent, then the {@link #build} may return null.
|
||||
*
|
||||
* @param context The context for playing the ringtone.
|
||||
* @param enabledMedia Which media to play. Media not included is implicitly muted.
|
||||
* @param audioAttributes The attributes to use for playback, which affects the volumes and
|
||||
* @param enabledMedia Which media to play. Media not included is implicitly muted. Device
|
||||
* settings such as volume and vibrate-only may also affect which
|
||||
* media is played.
|
||||
* @param audioAttributes The attributes to use for playback, which affects the volumes and
|
||||
* settings that are applied.
|
||||
*/
|
||||
public Builder(@NonNull Context context, @RingtoneMedia int enabledMedia,
|
||||
@NonNull AudioAttributes audioAttributes) {
|
||||
if (enabledMedia != MEDIA_SOUND) {
|
||||
throw new UnsupportedOperationException("Other media types not supported yet");
|
||||
}
|
||||
mContext = context;
|
||||
mEnabledMedia = enabledMedia;
|
||||
mAudioAttributes = audioAttributes;
|
||||
@@ -636,6 +672,27 @@ public class Ringtone {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the VibrationEffect to use if vibration is enabled on this ringtone. The caller
|
||||
* should use {@link android.os.Vibrator#areVibrationFeaturesSupported} to ensure
|
||||
* that the effect is usable on this device, otherwise system defaults will be used.
|
||||
*
|
||||
* <p>Vibration will only happen if the Builder was created with media type
|
||||
* {@link Ringtone#MEDIA_VIBRATION} or {@link Ringtone#MEDIA_SOUND_AND_VIBRATION}, and
|
||||
* the application has the {@link android.Manifest.permission#VIBRATE} permission.
|
||||
*
|
||||
* <p>If the Ringtone is looping when it is played, then the VibrationEffect will be
|
||||
* modified to loop. Similarly, if the ringtone is not looping, a repeating
|
||||
* VibrationEffect will be modified to be non-repeating when the ringtone is played. Calls
|
||||
* to {@link Ringtone#setLooping} after the ringtone has started playing will stop a looping
|
||||
* vibration, but has no effect otherwise: specifically it will not restart vibration.
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setVibrationEffect(@NonNull VibrationEffect effect) {
|
||||
mVibrationEffect = effect;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the resulting ringtone should loop until {@link Ringtone#stop()} is called,
|
||||
* or just play once.
|
||||
@@ -657,6 +714,17 @@ public class Ringtone {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to enable or disable the haptic generator.
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setEnableHapticGenerator(boolean enabled) {
|
||||
// Note that this property is mutable (but deprecated) on the Ringtone class itself.
|
||||
mHapticGeneratorEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial sound volume for the ringtone.
|
||||
*/
|
||||
@@ -667,12 +735,36 @@ public class Ringtone {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preferred device of the ringtone playback to the built-in device.
|
||||
* Sets the preferred device of the ringtone playback to the built-in device. This is
|
||||
* only for use by the system server with known-good Uris.
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setPreferBuiltinDevice() {
|
||||
mPreferBuiltinDevice = true;
|
||||
mAllowRemote = false; // Already in system.
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that {@link AudioAttributes#areHapticChannelsMuted()} on the builder's
|
||||
* AudioAttributes should not be overridden. This is used to enable legacy behavior of
|
||||
* calling {@link Ringtone#setAudioAttributes} on an already-created ringtone, and can in
|
||||
* turn cause vibration during a "sound-only" session or can suppress audio-coupled
|
||||
* haptics that would usually take priority (therefore potentially falling back to
|
||||
* the VibrationEffect or system defaults).
|
||||
*
|
||||
* <p>Without this setting, the haptic channels will be automatically muted or not by the
|
||||
* Ringtone according to whether vibration is enabled or not.
|
||||
*
|
||||
* <p>This is for internal-use only. New applications should configure the vibration
|
||||
* behavior explicitly with the (TODO: future RingtoneSetting.setVibrationSource).
|
||||
* Handling haptic channels outside Ringtone leads to extra loads of the sound uri.
|
||||
* @hide
|
||||
*/
|
||||
@NonNull
|
||||
public Builder setUseExactAudioAttributes(boolean useExactAttrs) {
|
||||
mUseExactAudioAttributes = useExactAttrs;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -687,14 +779,77 @@ public class Ringtone {
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isVibrationEnabledAndAvailable() {
|
||||
if ((mEnabledMedia & MEDIA_VIBRATION) == 0) {
|
||||
return false;
|
||||
}
|
||||
Vibrator vibrator = mContext.getSystemService(Vibrator.class);
|
||||
if (!vibrator.hasVibrator()) {
|
||||
return false;
|
||||
}
|
||||
if (mContext.checkSelfPermission(Manifest.permission.VIBRATE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "Ringtone requests vibration enabled, but no VIBRATE permission");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the built Ringtone, or null if there was a problem loading the Uri and there
|
||||
* are no fallback options available.
|
||||
*/
|
||||
@Nullable
|
||||
public Ringtone build() {
|
||||
@Ringtone.RingtoneMedia int effectiveEnabledMedia = mEnabledMedia;
|
||||
VibrationEffect effectiveVibrationEffect = mVibrationEffect;
|
||||
|
||||
// Normalize media to that supported on this SDK level.
|
||||
if (effectiveEnabledMedia != (effectiveEnabledMedia & MEDIA_ALL)) {
|
||||
Log.e(TAG, "Unsupported media type: " + effectiveEnabledMedia);
|
||||
effectiveEnabledMedia = effectiveEnabledMedia & MEDIA_ALL;
|
||||
}
|
||||
final boolean effectiveHapticGenerator;
|
||||
final boolean hapticChannelsSupported;
|
||||
AudioAttributes effectiveAudioAttributes = mAudioAttributes;
|
||||
final boolean hapticChannelsMuted = mAudioAttributes.areHapticChannelsMuted();
|
||||
if (!isVibrationEnabledAndAvailable()) {
|
||||
// Vibration isn't active: turn off everything that might cause extra work.
|
||||
effectiveEnabledMedia &= ~MEDIA_VIBRATION;
|
||||
effectiveHapticGenerator = false;
|
||||
effectiveVibrationEffect = null;
|
||||
if (!mUseExactAudioAttributes && !hapticChannelsMuted) {
|
||||
effectiveAudioAttributes = new AudioAttributes.Builder(effectiveAudioAttributes)
|
||||
.setHapticChannelsMuted(true)
|
||||
.build();
|
||||
}
|
||||
} else {
|
||||
// Vibration is active.
|
||||
effectiveHapticGenerator =
|
||||
mHapticGeneratorEnabled && mInjectables.isHapticGeneratorAvailable();
|
||||
hapticChannelsSupported = mInjectables.isHapticPlaybackSupported();
|
||||
// Haptic channels are preferred if they are available, and not explicitly muted.
|
||||
// We won't know if haptic channels are available until loading the media player,
|
||||
// and since the media player needs to be reset to change audio attributes, then
|
||||
// we proactively enable the channels - it won't matter if they aren't present.
|
||||
if (!mUseExactAudioAttributes) {
|
||||
boolean shouldBeMuted = effectiveHapticGenerator || !hapticChannelsSupported;
|
||||
if (shouldBeMuted != hapticChannelsMuted) {
|
||||
effectiveAudioAttributes =
|
||||
new AudioAttributes.Builder(effectiveAudioAttributes)
|
||||
.setHapticChannelsMuted(shouldBeMuted)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
// If no contextual vibration, then try loading the default one for the URI.
|
||||
if (mVibrationEffect == null && mUri != null) {
|
||||
effectiveVibrationEffect = VibrationEffect.get(mUri, mContext);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Ringtone ringtone = new Ringtone(this);
|
||||
Ringtone ringtone = new Ringtone(this, effectiveEnabledMedia,
|
||||
effectiveAudioAttributes, effectiveVibrationEffect,
|
||||
effectiveHapticGenerator);
|
||||
if (ringtone.reinitializeActivePlayer()) {
|
||||
return ringtone;
|
||||
} else {
|
||||
@@ -706,7 +861,7 @@ public class Ringtone {
|
||||
// RingtoneManager.getRingtone and hides errors like DocumentsProvider throwing
|
||||
// IllegalArgumentException instead of FileNotFoundException, and also robolectric
|
||||
// failures when ShadowMediaPlayer wasn't pre-informed of the ringtone.
|
||||
Log.e(TAG, "Failed to open ringtone " + mUri + ": " + ex);
|
||||
Log.e(TAG, "Failed while opening ringtone " + mUri, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -744,21 +899,29 @@ public class Ringtone {
|
||||
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 mIsHapticGeneratorEnabled;
|
||||
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 isHapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
|
||||
boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
|
||||
mRemoteRingtoneService = remoteRingtoneService;
|
||||
mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri();
|
||||
mAudioAttributes = audioAttributes;
|
||||
mUseExactAudioAttributes = useExactAudioAttributes;
|
||||
mEnabledMedia = enabledMedia;
|
||||
mVibrationEffect = vibrationEffect;
|
||||
mVolumeShaperConfig = volumeShaperConfig;
|
||||
mIsHapticGeneratorEnabled = isHapticGeneratorEnabled;
|
||||
mHapticGeneratorEnabled = hapticGeneratorEnabled;
|
||||
mIsLooping = initialIsLooping;
|
||||
mVolume = initialVolume;
|
||||
}
|
||||
@@ -766,9 +929,9 @@ public class Ringtone {
|
||||
@Override
|
||||
public boolean play() {
|
||||
try {
|
||||
mRemoteRingtoneService.playWithVolumeShaping(mRemoteToken, mCanonicalUri,
|
||||
mAudioAttributes, mVolume, mIsLooping, mIsHapticGeneratorEnabled,
|
||||
mVolumeShaperConfig);
|
||||
mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri,
|
||||
mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
|
||||
mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig);
|
||||
return true;
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Problem playing ringtone: " + e);
|
||||
@@ -812,7 +975,7 @@ public class Ringtone {
|
||||
|
||||
@Override
|
||||
public void setHapticGeneratorEnabled(boolean enabled) {
|
||||
mIsHapticGeneratorEnabled = enabled;
|
||||
mHapticGeneratorEnabled = enabled;
|
||||
try {
|
||||
mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled);
|
||||
} catch (RemoteException e) {
|
||||
@@ -863,5 +1026,30 @@ public class Ringtone {
|
||||
public HapticGenerator createHapticGenerator(@NonNull MediaPlayer mediaPlayer) {
|
||||
return HapticGenerator.create(mediaPlayer.getAudioSessionId());
|
||||
}
|
||||
|
||||
/** Returns the result of {@link AudioManager#isHapticPlaybackSupported()}. */
|
||||
public boolean isHapticPlaybackSupported() {
|
||||
return AudioManager.isHapticPlaybackSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the MediaPlayer tracks have haptic channels. This is the same as
|
||||
* AudioManager.hasHapticChannels, except it uses an already prepared MediaPlayer to avoid
|
||||
* loading the metadata a second time.
|
||||
*/
|
||||
public boolean hasHapticChannels(MediaPlayer mp) {
|
||||
try {
|
||||
Trace.beginSection("Ringtone.hasHapticChannels");
|
||||
for (MediaPlayer.TrackInfo trackInfo : mp.getTrackInfo()) {
|
||||
if (trackInfo.hasHapticChannels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,6 +709,7 @@ public class RingtoneManager {
|
||||
return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes)
|
||||
.setUri(ringtoneUri)
|
||||
.setVolumeShaperConfig(volumeShaperConfig)
|
||||
.setUseExactAudioAttributes(true) // May be using audio-coupled via attrs
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,21 +16,30 @@
|
||||
|
||||
package com.android.mediaframeworktest.unit;
|
||||
|
||||
import static android.media.Ringtone.MEDIA_SOUND;
|
||||
import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
@@ -40,8 +49,12 @@ import android.media.Ringtone;
|
||||
import android.media.audiofx.HapticGenerator;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.os.VibrationAttributes;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.testing.TestableContext;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
@@ -73,13 +86,24 @@ public class RingtoneTest {
|
||||
|
||||
private static final AudioAttributes RINGTONE_ATTRIBUTES =
|
||||
audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
|
||||
private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC =
|
||||
new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build();
|
||||
private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES =
|
||||
new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build();
|
||||
|
||||
private static final VibrationEffect VIBRATION_EFFECT =
|
||||
VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1);
|
||||
private static final VibrationEffect VIBRATION_EFFECT_REPEATING =
|
||||
VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1);
|
||||
|
||||
@Rule
|
||||
public final RingtoneInjectablesTrackingTestRule
|
||||
mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule();
|
||||
|
||||
@Captor ArgumentCaptor<IBinder> mIBinderCaptor;
|
||||
@Mock IRingtonePlayer mMockRemotePlayer;
|
||||
@Captor private ArgumentCaptor<IBinder> mIBinderCaptor;
|
||||
@Mock private IRingtonePlayer mMockRemotePlayer;
|
||||
@Mock private Vibrator mMockVibrator;
|
||||
private AudioManager mSpyAudioManager;
|
||||
private TestableContext mContext;
|
||||
|
||||
@Before
|
||||
@@ -87,11 +111,13 @@ public class RingtoneTest {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
TestableContext testContext =
|
||||
new TestableContext(InstrumentationRegistry.getTargetContext(), null);
|
||||
|
||||
testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE,
|
||||
PackageManager.PERMISSION_GRANTED);
|
||||
AudioManager realAudioManager = testContext.getSystemService(AudioManager.class);
|
||||
AudioManager spyAudioManager = spy(realAudioManager);
|
||||
when(spyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer);
|
||||
testContext.addMockSystemService(Context.AUDIO_SERVICE, spyAudioManager);
|
||||
mSpyAudioManager = spy(realAudioManager);
|
||||
when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer);
|
||||
testContext.addMockSystemService(AudioManager.class, mSpyAudioManager);
|
||||
testContext.addMockSystemService(Vibrator.class, mMockVibrator);
|
||||
|
||||
mContext = spy(testContext);
|
||||
}
|
||||
@@ -100,12 +126,12 @@ public class RingtoneTest {
|
||||
public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception {
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
Ringtone ringtone =
|
||||
newBuilder(Ringtone.MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build();
|
||||
newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isFalse();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(Ringtone.MEDIA_SOUND);
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
|
||||
assertThat(ringtone.getVolume()).isEqualTo(1.0f);
|
||||
@@ -116,8 +142,7 @@ public class RingtoneTest {
|
||||
assertThat(ringtone.isLocalOnly()).isFalse();
|
||||
|
||||
// Prepare
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI,
|
||||
audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE));
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES);
|
||||
verify(mockMediaPlayer).setVolume(1.0f);
|
||||
verify(mockMediaPlayer).setLooping(false);
|
||||
verify(mockMediaPlayer).prepare();
|
||||
@@ -129,7 +154,9 @@ public class RingtoneTest {
|
||||
// Verify dynamic controls.
|
||||
ringtone.setVolume(0.8f);
|
||||
verify(mockMediaPlayer).setVolume(0.8f);
|
||||
when(mockMediaPlayer.isLooping()).thenReturn(false);
|
||||
ringtone.setLooping(true);
|
||||
verify(mockMediaPlayer).isLooping();
|
||||
verify(mockMediaPlayer).setLooping(true);
|
||||
HapticGenerator mockHapticGenerator =
|
||||
mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer);
|
||||
@@ -139,10 +166,52 @@ public class RingtoneTest {
|
||||
// Release
|
||||
ringtone.stop();
|
||||
verifyLocalStop(mockMediaPlayer);
|
||||
|
||||
// This test is intended to strictly verify all interactions with MediaPlayer in a local
|
||||
// playback case. This shouldn't be necessary in other tests that have the same basic
|
||||
// setup.
|
||||
verifyNoMoreInteractions(mockMediaPlayer);
|
||||
verify(mockHapticGenerator).release();
|
||||
verifyNoMoreInteractions(mockHapticGenerator);
|
||||
verifyZeroInteractions(mMockRemotePlayer);
|
||||
verifyZeroInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception {
|
||||
// Audio coupled playback is enabled in the incoming attributes, plus an instruction
|
||||
// to leave the attributes alone. This test verifies that the attributes reach the
|
||||
// media player without changing.
|
||||
final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC;
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
|
||||
Ringtone ringtone =
|
||||
newBuilder(MEDIA_SOUND, audioAttributes)
|
||||
.setUri(SOUND_URI)
|
||||
.setUseExactAudioAttributes(true)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isFalse();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes);
|
||||
|
||||
// Prepare
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes);
|
||||
verify(mockMediaPlayer).prepare();
|
||||
|
||||
// Play
|
||||
ringtone.play();
|
||||
verifyLocalPlay(mockMediaPlayer);
|
||||
|
||||
// Release
|
||||
ringtone.stop();
|
||||
verifyLocalStop(mockMediaPlayer);
|
||||
|
||||
verifyZeroInteractions(mMockRemotePlayer);
|
||||
verifyZeroInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -150,14 +219,14 @@ public class RingtoneTest {
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
setupFileNotFound(mockMediaPlayer, SOUND_URI);
|
||||
Ringtone ringtone =
|
||||
newBuilder(Ringtone.MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
.setUri(SOUND_URI)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isTrue();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(Ringtone.MEDIA_SOUND);
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
|
||||
assertThat(ringtone.getVolume()).isEqualTo(1.0f);
|
||||
@@ -174,8 +243,8 @@ public class RingtoneTest {
|
||||
|
||||
// Delegates to remote media player.
|
||||
ringtone.play();
|
||||
verify(mMockRemotePlayer).playWithVolumeShaping(mIBinderCaptor.capture(), eq(SOUND_URI),
|
||||
eq(audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)),
|
||||
verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI),
|
||||
eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(),
|
||||
eq(1.0f), eq(false), eq(false), isNull());
|
||||
IBinder remoteToken = mIBinderCaptor.getValue();
|
||||
|
||||
@@ -191,6 +260,148 @@ public class RingtoneTest {
|
||||
verify(mMockRemotePlayer).stop(remoteToken);
|
||||
verifyNoMoreInteractions(mMockRemotePlayer);
|
||||
verifyNoMoreInteractions(mockMediaPlayer);
|
||||
verifyZeroInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRingtone_localMediaWithVibration() throws Exception {
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
when(mMockVibrator.hasVibrator()).thenReturn(true);
|
||||
Ringtone ringtone =
|
||||
newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
|
||||
.setUri(SOUND_URI)
|
||||
.setVibrationEffect(VIBRATION_EFFECT)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isFalse();
|
||||
verify(mMockVibrator).hasVibrator();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
|
||||
|
||||
// Prepare
|
||||
// Uses attributes with haptic channels enabled, but will use the effect when there aren't
|
||||
// any present.
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
|
||||
verify(mockMediaPlayer).setVolume(1.0f);
|
||||
verify(mockMediaPlayer).setLooping(false);
|
||||
verify(mockMediaPlayer).prepare();
|
||||
|
||||
// Play
|
||||
ringtone.play();
|
||||
|
||||
verifyLocalPlay(mockMediaPlayer);
|
||||
verify(mockMediaPlayer).isLooping(); // When starting the vibration.
|
||||
verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
|
||||
|
||||
// Verify dynamic controls.
|
||||
ringtone.setVolume(0.8f);
|
||||
verify(mockMediaPlayer).setVolume(0.8f);
|
||||
|
||||
// Set looping doesn't affect an already-started vibration.
|
||||
when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original
|
||||
ringtone.setLooping(true);
|
||||
verify(mockMediaPlayer).isPlaying(); // Vibration check.
|
||||
verify(mockMediaPlayer, times(2)).isLooping(); // Current state, second isLooping call.
|
||||
verify(mockMediaPlayer).setLooping(true);
|
||||
|
||||
// This is ignored because there's a vibration effect being used.
|
||||
ringtone.setHapticGeneratorEnabled(true);
|
||||
|
||||
// Release
|
||||
ringtone.stop();
|
||||
verifyLocalStop(mockMediaPlayer);
|
||||
verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
|
||||
|
||||
// This test is intended to strictly verify all interactions with MediaPlayer in a local
|
||||
// playback case. This shouldn't be necessary in other tests that have the same basic
|
||||
// setup.
|
||||
verifyNoMoreInteractions(mockMediaPlayer);
|
||||
verifyZeroInteractions(mMockRemotePlayer);
|
||||
verifyNoMoreInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception {
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
|
||||
when(mMockVibrator.hasVibrator()).thenReturn(true);
|
||||
Ringtone ringtone =
|
||||
newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
|
||||
.setUri(SOUND_URI)
|
||||
.setVibrationEffect(VIBRATION_EFFECT)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isFalse();
|
||||
verify(mMockVibrator).hasVibrator();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
|
||||
|
||||
// Prepare
|
||||
// The attributes here have haptic channels enabled (unlike above)
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
|
||||
verify(mockMediaPlayer).prepare();
|
||||
|
||||
// Play
|
||||
ringtone.play();
|
||||
when(mockMediaPlayer.isPlaying()).thenReturn(true);
|
||||
verifyLocalPlay(mockMediaPlayer);
|
||||
|
||||
// Release
|
||||
ringtone.stop();
|
||||
verifyLocalStop(mockMediaPlayer);
|
||||
|
||||
verifyZeroInteractions(mMockRemotePlayer);
|
||||
// Nothing after the initial hasVibrator - it uses audio-coupled.
|
||||
verifyNoMoreInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception {
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false);
|
||||
doReturn(0).when(mSpyAudioManager)
|
||||
.getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES));
|
||||
when(mMockVibrator.hasVibrator()).thenReturn(true);
|
||||
Ringtone ringtone =
|
||||
newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
|
||||
.setUri(SOUND_URI)
|
||||
.setVibrationEffect(VIBRATION_EFFECT)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isFalse();
|
||||
verify(mMockVibrator).hasVibrator();
|
||||
|
||||
// Verify all the properties.
|
||||
assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
|
||||
assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
|
||||
assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
|
||||
|
||||
// Prepare
|
||||
// The attributes here have haptic channels enabled (unlike above)
|
||||
verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
|
||||
verify(mockMediaPlayer).prepare();
|
||||
|
||||
// Play
|
||||
ringtone.play();
|
||||
// The media player is never played, because sound is muted.
|
||||
verify(mockMediaPlayer, never()).start();
|
||||
when(mockMediaPlayer.isPlaying()).thenReturn(true);
|
||||
verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
|
||||
|
||||
// Release
|
||||
ringtone.stop();
|
||||
verify(mockMediaPlayer).release();
|
||||
verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
|
||||
|
||||
verifyZeroInteractions(mMockRemotePlayer);
|
||||
// Nothing after the initial hasVibrator - it uses audio-coupled.
|
||||
verifyNoMoreInteractions(mMockVibrator);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -204,7 +415,7 @@ public class RingtoneTest {
|
||||
.addOverride(com.android.internal.R.raw.fallbackring, testResourceFd);
|
||||
|
||||
MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
|
||||
Ringtone ringtone = newBuilder(Ringtone.MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
.setUri(null)
|
||||
.build();
|
||||
assertThat(ringtone).isNotNull();
|
||||
@@ -233,7 +444,7 @@ public class RingtoneTest {
|
||||
public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception {
|
||||
mContext.getOrCreateTestableResources()
|
||||
.addOverride(com.android.internal.R.raw.fallbackring, null);
|
||||
Ringtone ringtone = newBuilder(Ringtone.MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
.setUri(null)
|
||||
.setLooping(true) // distinct from haptic generator, to match plumbing
|
||||
.build();
|
||||
@@ -243,8 +454,9 @@ public class RingtoneTest {
|
||||
assertThat(ringtone.isUsingRemotePlayer()).isTrue();
|
||||
|
||||
ringtone.play();
|
||||
verify(mMockRemotePlayer).playWithVolumeShaping(mIBinderCaptor.capture(), isNull(),
|
||||
eq(audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)),
|
||||
verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(),
|
||||
eq(RINGTONE_ATTRIBUTES), eq(false),
|
||||
eq(MEDIA_SOUND), isNull(),
|
||||
eq(1.0f), eq(true), eq(false), isNull());
|
||||
ringtone.stop();
|
||||
verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue());
|
||||
@@ -255,7 +467,7 @@ public class RingtoneTest {
|
||||
public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception {
|
||||
mContext.getOrCreateTestableResources()
|
||||
.addOverride(com.android.internal.R.raw.fallbackring, null);
|
||||
Ringtone ringtone = newBuilder(Ringtone.MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
|
||||
.setUri(null)
|
||||
.setLocalOnly()
|
||||
.build();
|
||||
@@ -308,11 +520,12 @@ public class RingtoneTest {
|
||||
}
|
||||
|
||||
private void verifyLocalPlay(MediaPlayer mockMediaPlayer) {
|
||||
verify(mockMediaPlayer).setOnCompletionListener(anyObject());
|
||||
verify(mockMediaPlayer).setOnCompletionListener(any());
|
||||
verify(mockMediaPlayer).start();
|
||||
}
|
||||
|
||||
private void verifyLocalStop(MediaPlayer mockMediaPlayer) {
|
||||
verify(mockMediaPlayer).stop();
|
||||
verify(mockMediaPlayer).setOnCompletionListener(isNull());
|
||||
verify(mockMediaPlayer).reset();
|
||||
verify(mockMediaPlayer).release();
|
||||
@@ -340,6 +553,9 @@ public class RingtoneTest {
|
||||
// Similar to media players, but for haptic generator, which also needs releasing.
|
||||
private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>();
|
||||
|
||||
// Media players with haptic channels.
|
||||
private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>();
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@@ -357,8 +573,15 @@ public class RingtoneTest {
|
||||
};
|
||||
}
|
||||
|
||||
private MediaPlayer expectLocalMediaPlayer() {
|
||||
MediaPlayer mockMediaPlayer = Mockito.mock(MediaPlayerMockableNatives.class);
|
||||
private TestMediaPlayer expectLocalMediaPlayer() {
|
||||
TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class);
|
||||
// Delegate to simulated methods. This means they can be verified but also reflect
|
||||
// realistic transitions from the TestMediaPlayer.
|
||||
doCallRealMethod().when(mockMediaPlayer).start();
|
||||
doCallRealMethod().when(mockMediaPlayer).stop();
|
||||
doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean());
|
||||
when(mockMediaPlayer.isLooping()).thenCallRealMethod();
|
||||
when(mockMediaPlayer.isLooping()).thenCallRealMethod();
|
||||
mMockMediaPlayerQueue.add(mockMediaPlayer);
|
||||
return mockMediaPlayer;
|
||||
}
|
||||
@@ -373,6 +596,14 @@ public class RingtoneTest {
|
||||
return mockHapticGenerator;
|
||||
}
|
||||
|
||||
private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) {
|
||||
if (hasHapticChannels) {
|
||||
mHapticChannels.add(mp);
|
||||
} else {
|
||||
mHapticChannels.remove(mp);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestInjectables extends Ringtone.Injectables {
|
||||
@Override
|
||||
public MediaPlayer newMediaPlayer() {
|
||||
@@ -397,14 +628,61 @@ public class RingtoneTest {
|
||||
.isNotNull();
|
||||
return mockHapticGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHapticPlaybackSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHapticChannels(MediaPlayer mp) {
|
||||
return mHapticChannels.contains(mp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Mocks don't work directly on native calls, but if they're overridden then it does work. */
|
||||
private static class MediaPlayerMockableNatives extends MediaPlayer {
|
||||
/**
|
||||
* MediaPlayer relies on a native backend and so its necessary to intercept calls from
|
||||
* fake usage hitting them.
|
||||
*
|
||||
* Mocks don't work directly on native calls, but if they're overridden then it does work.
|
||||
* Some basic state faking is also done to make the mocks more realistic.
|
||||
*/
|
||||
private static class TestMediaPlayer extends MediaPlayer {
|
||||
private boolean mIsPlaying = false;
|
||||
private boolean mIsLooping = false;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mIsPlaying = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
mIsPlaying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLooping(boolean value) {
|
||||
throw new IllegalStateException("Expected mock to intercept");
|
||||
mIsLooping = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLooping() {
|
||||
return mIsLooping;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mIsPlaying;
|
||||
}
|
||||
|
||||
void simulatePlayingFinished() {
|
||||
if (!mIsPlaying) {
|
||||
throw new IllegalStateException(
|
||||
"Attempted to pretend playing finished when not playing");
|
||||
}
|
||||
mIsPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.systemui.media;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -34,6 +37,7 @@ import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -86,21 +90,11 @@ public class RingtonePlayer implements CoreStartable {
|
||||
*/
|
||||
private class Client implements IBinder.DeathRecipient {
|
||||
private final IBinder mToken;
|
||||
private final Ringtone mRingtone;
|
||||
private Ringtone mRingtone;
|
||||
|
||||
public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) {
|
||||
this(token, uri, user, aa, null);
|
||||
}
|
||||
|
||||
Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa,
|
||||
@Nullable VolumeShaper.Configuration volumeShaperConfig) {
|
||||
mToken = token;
|
||||
|
||||
mRingtone = new Ringtone.Builder(getContextForUser(user), Ringtone.MEDIA_SOUND, aa)
|
||||
.setUri(uri)
|
||||
.setLocalOnly()
|
||||
.setVolumeShaperConfig(volumeShaperConfig)
|
||||
.build();
|
||||
Client(@NonNull IBinder token, @NonNull Ringtone ringtone) {
|
||||
mToken = requireNonNull(token);
|
||||
mRingtone = requireNonNull(ringtone);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,12 +111,16 @@ public class RingtonePlayer implements CoreStartable {
|
||||
@Override
|
||||
public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
|
||||
throws RemoteException {
|
||||
playWithVolumeShaping(token, uri, aa, volume, looping, /* hapticGenerator= */ false,
|
||||
playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
|
||||
null, volume, looping, /* hapticGenerator= */ false,
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume,
|
||||
public void playRemoteRingtone(IBinder token, Uri uri, AudioAttributes aa,
|
||||
boolean useExactAudioAttributes,
|
||||
@Ringtone.RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
|
||||
float volume,
|
||||
boolean looping, boolean isHapticGeneratorEnabled,
|
||||
@Nullable VolumeShaper.Configuration volumeShaperConfig)
|
||||
throws RemoteException {
|
||||
@@ -130,19 +128,39 @@ public class RingtonePlayer implements CoreStartable {
|
||||
Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
|
||||
+ Binder.getCallingUid() + ")");
|
||||
}
|
||||
|
||||
// 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.
|
||||
Client client;
|
||||
synchronized (mClients) {
|
||||
client = mClients.get(token);
|
||||
if (client == null) {
|
||||
final UserHandle user = Binder.getCallingUserHandle();
|
||||
client = new Client(token, uri, user, aa, volumeShaperConfig);
|
||||
token.linkToDeath(client, 0);
|
||||
mClients.put(token, client);
|
||||
}
|
||||
if (client == null) {
|
||||
final UserHandle user = Binder.getCallingUserHandle();
|
||||
Ringtone ringtone = new Ringtone.Builder(getContextForUser(user), enabledMedia, aa)
|
||||
.setLocalOnly()
|
||||
.setUri(uri)
|
||||
.setLooping(looping)
|
||||
.setInitialSoundVolume(volume)
|
||||
.setUseExactAudioAttributes(useExactAudioAttributes)
|
||||
.setEnableHapticGenerator(isHapticGeneratorEnabled)
|
||||
.setVibrationEffect(vibrationEffect)
|
||||
.setVolumeShaperConfig(volumeShaperConfig)
|
||||
.build();
|
||||
if (ringtone == null) {
|
||||
return;
|
||||
}
|
||||
synchronized (mClients) {
|
||||
client = mClients.get(token);
|
||||
if (client == null) {
|
||||
client = new Client(token, ringtone);
|
||||
token.linkToDeath(client, 0);
|
||||
mClients.put(token, client);
|
||||
}
|
||||
}
|
||||
}
|
||||
client.mRingtone.setLooping(looping);
|
||||
client.mRingtone.setVolume(volume);
|
||||
client.mRingtone.setHapticGeneratorEnabled(isHapticGeneratorEnabled);
|
||||
// Ensure the client is initialized outside the all-clients lock, as it can be slow.
|
||||
client.mRingtone.play();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user