diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 8edb8213248d3..af3fc15e3f105 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -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;
*
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;
}
diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl
index 97cc1a8d4a545..b3f72a1aa2bb2 100644
--- a/media/java/android/media/IRingtonePlayer.aidl
+++ b/media/java/android/media/IRingtonePlayer.aidl
@@ -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);
diff --git a/media/java/android/media/LocalRingtonePlayer.java b/media/java/android/media/LocalRingtonePlayer.java
index 4aa24af9d444e..4b41a6aa8ab54 100644
--- a/media/java/android/media/LocalRingtonePlayer.java
+++ b/media/java/android/media/LocalRingtonePlayer.java
@@ -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 sActiveRingtones = new ArrayList<>();
+ private static final ArrayList 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.
}
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 4be88fb23badd..2d9d411320f81 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -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.
+ *
+ * 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.
+ *
+ *
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).
+ *
+ *
Without this setting, the haptic channels will be automatically muted or not by the
+ * Ringtone according to whether vibration is enabled or not.
+ *
+ *
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;
+ }
+
}
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index ebf02a26672b1..12766fbb6c9da 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -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();
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
index d8e3f8d37c096..f96a83a1a70c1 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
@@ -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 mIBinderCaptor;
- @Mock IRingtonePlayer mMockRemotePlayer;
+ @Captor private ArgumentCaptor 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 mMockHapticGeneratorMap = new ArrayMap<>();
+ // Media players with haptic channels.
+ private ArraySet 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;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index d4b30d3913ef1..6d9844d9ec595 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -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();
}