Loading media/java/android/media/LocalRingtonePlayer.java +135 −55 Original line number Diff line number Diff line Loading @@ -39,27 +39,23 @@ 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> sActiveMediaPlayers = new ArrayList<>(); private final MediaPlayer mMediaPlayer; private final AudioAttributes mAudioAttributes; private final VibrationAttributes mVibrationAttributes; private final Ringtone.RingtonePlayer mVibrationPlayer; 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, @NonNull Vibrator vibrator, @Nullable VibrationEffect vibrationEffect) { @Nullable VolumeShaper volumeShaper, @Nullable Ringtone.RingtonePlayer vibrationPlayer) { Objects.requireNonNull(mediaPlayer); Objects.requireNonNull(audioAttributes); Objects.requireNonNull(injectables); Loading @@ -69,11 +65,8 @@ public class LocalRingtonePlayer mInjectables = injectables; mAudioManager = audioManager; mVolumeShaper = volumeShaper; mVibrator = vibrator; mVibrationEffect = vibrationEffect; mVibrationPlayer = vibrationPlayer; mHapticGenerator = hapticGenerator; mVibrationAttributes = (mVibrationEffect == null) ? null : new VibrationAttributes.Builder(audioAttributes).build(); } /** Loading @@ -81,9 +74,11 @@ public class LocalRingtonePlayer * loaded in the local player. */ @Nullable static LocalRingtonePlayer create(@NonNull Context context, @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri, static Ringtone.RingtonePlayer create(@NonNull Context context, @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri, @NonNull AudioAttributes audioAttributes, boolean isVibrationOnly, @Nullable VibrationEffect vibrationEffect, @NonNull Ringtone.Injectables injectables, @Nullable VolumeShaper.Configuration volumeShaperConfig, Loading @@ -100,7 +95,7 @@ public class LocalRingtonePlayer mediaPlayer.setAudioAttributes(audioAttributes); mediaPlayer.setPreferredDevice(preferredDevice); mediaPlayer.setLooping(initialLooping); mediaPlayer.setVolume(initialVolume); mediaPlayer.setVolume(isVibrationOnly ? 0 : initialVolume); if (initialHapticGeneratorEnabled) { hapticGenerator = injectables.createHapticGenerator(mediaPlayer); if (hapticGenerator != null) { Loading @@ -121,8 +116,16 @@ public class LocalRingtonePlayer vibrationEffect = null; } } VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : new VibrationEffectPlayer( vibrationEffect, audioAttributes, vibrator, initialLooping); if (isVibrationOnly && vibrationEffectPlayer != null) { // Abandon the media player now that it's confirmed to not have haptic channels. mediaPlayer.release(); return vibrationEffectPlayer; } return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, hapticGenerator, volumeShaper, vibrator, vibrationEffect); hapticGenerator, volumeShaper, vibrationEffectPlayer); } catch (SecurityException | IOException e) { if (hapticGenerator != null) { hapticGenerator.release(); Loading Loading @@ -179,8 +182,11 @@ public class LocalRingtonePlayer vibrationEffect = null; } } VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : new VibrationEffectPlayer( vibrationEffect, audioAttributes, vibrator, initialLooping); return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, /* hapticGenerator= */ null, volumeShaper, vibrator, vibrationEffect); /* hapticGenerator= */ null, volumeShaper, vibrationEffectPlayer); } catch (SecurityException | IOException e) { Log.e(TAG, "Failed to open fallback ringtone"); // TODO: vibration-effect-only / no-sound LocalRingtonePlayer. Loading Loading @@ -217,33 +223,8 @@ public class LocalRingtonePlayer } 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); } if (mVibrationPlayer != null) { mVibrationPlayer.play(); } } Loading @@ -260,7 +241,13 @@ public class LocalRingtonePlayer try { mMediaPlayer.stop(); } finally { stopVibration(); // Won't throw: catches exceptions. if (mVibrationPlayer != null) { try { mVibrationPlayer.stopAndRelease(); } catch (Exception e) { Log.e(TAG, "Exception stopping ringtone vibration", e); } } if (mHapticGenerator != null) { mHapticGenerator.release(); } Loading @@ -282,25 +269,19 @@ public class LocalRingtonePlayer 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(); } if (mVibrationPlayer != null) { mVibrationPlayer.setLooping(looping); } } @Override public void setHapticGeneratorEnabled(boolean enabled) { if (mVibrationEffect != null) { // Ignore haptic generator changes if a vibration effect is present. The decision to if (mVibrationPlayer != null) { // Ignore haptic generator changes if a vibration player is present. The decision to // use one or the other happens before this object is constructed. return; } if (enabled && mHapticGenerator == null) { if (enabled && mHapticGenerator == null && !hasHapticChannels()) { mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer); } if (mHapticGenerator != null) { Loading @@ -311,6 +292,7 @@ public class LocalRingtonePlayer @Override public void setVolume(float volume) { mMediaPlayer.setVolume(volume); // no effect on vibration player } @Override Loading @@ -324,5 +306,103 @@ public class LocalRingtonePlayer sActiveMediaPlayers.remove(this); } mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. // No effect on vibration: either it's looping and this callback only happens when stopped, // or it's not looping, in which case the vibration should play to its own completion. } /** A RingtonePlayer that only plays a VibrationEffect. */ static class VibrationEffectPlayer implements Ringtone.RingtonePlayer { private static final int VIBRATION_LOOP_DELAY_MS = 200; private final VibrationEffect mVibrationEffect; private final VibrationAttributes mVibrationAttributes; private final Vibrator mVibrator; private boolean mIsLooping; private boolean mStartedVibration; VibrationEffectPlayer(@NonNull VibrationEffect vibrationEffect, @NonNull AudioAttributes audioAttributes, @NonNull Vibrator vibrator, boolean initialLooping) { mVibrationEffect = vibrationEffect; mVibrationAttributes = new VibrationAttributes.Builder(audioAttributes).build(); mVibrator = vibrator; mIsLooping = initialLooping; } @Override public boolean play() { if (!mStartedVibration) { try { // Adjust the vibration effect to loop. VibrationEffect loopAdjustedEffect = mVibrationEffect.applyRepeatingIndefinitely( mIsLooping, 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 " + (mIsLooping ? "looping " : "") + "vibration " + "for ringtone: " + mVibrationEffect, e); return false; } } return true; } @Override public boolean isPlaying() { return mStartedVibration; } @Override public void stopAndRelease() { if (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 void setPreferredDevice(AudioDeviceInfo audioDeviceInfo) { // no-op } @Override public void setLooping(boolean looping) { if (looping == mIsLooping) { return; } mIsLooping = looping; if (mStartedVibration) { if (!mIsLooping) { // Was looping, stop looping stopAndRelease(); } // Else was not looping, but can't interfere with a running vibration without // restarting it, and don't know if it was finished. So do nothing: apps shouldn't // toggle looping after calling play anyway. } } @Override public void setHapticGeneratorEnabled(boolean enabled) { // n/a } @Override public void setVolume(float volume) { // n/a } @Override public boolean hasHapticChannels() { return false; } } } media/java/android/media/Ringtone.java +21 −2 Original line number Diff line number Diff line Loading @@ -286,14 +286,29 @@ public class Ringtone { stopAndReleaseActivePlayer(); } boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION; // Vibration can come from the audio file if using haptic generator or if haptic // channels are a possibility. boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported() && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted()); // VibrationEffect only, use the simplified player without checking for haptic channels. if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) { mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer( mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping); return true; } AudioDeviceInfo preferredDevice = mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null; if (mUri != null) { mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri, mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping, mVolume); mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables, mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping, mVolume); } else { // Using the remote player won't help play a null Uri. Revert straight to fallback. // The vibration-only case was already covered above. mActivePlayer = createFallbackRingtonePlayer(); // Fall through to attempting remote fallback play if null. } Loading Loading @@ -388,6 +403,10 @@ public class Ringtone { * corresponds to no attenuation being applied. */ public void setVolume(float volume) { // Ignore if sound not enabled. if ((mEnabledMedia & MEDIA_SOUND) == 0) { return; } if (volume < 0.0f) { volume = 0.0f; } else if (volume > 1.0f) { Loading media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java +156 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.mediaframeworktest.unit; import static android.media.Ringtone.MEDIA_SOUND; import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION; import static android.media.Ringtone.MEDIA_VIBRATION; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; Loading @@ -31,7 +32,6 @@ 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; Loading Loading @@ -293,7 +293,6 @@ public class RingtoneTest { ringtone.play(); verifyLocalPlay(mockMediaPlayer); verify(mockMediaPlayer).isLooping(); // When starting the vibration. verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls. Loading @@ -303,8 +302,7 @@ public class RingtoneTest { // 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).isLooping(); verify(mockMediaPlayer).setLooping(true); // This is ignored because there's a vibration effect being used. Loading @@ -323,6 +321,160 @@ public class RingtoneTest { verifyNoMoreInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationOnly() throws Exception { when(mMockVibrator.hasVibrator()).thenReturn(true); Ringtone ringtone = newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) // TODO: set sound uri too in diff test .setVibrationEffect(VIBRATION_EFFECT) .build(); assertThat(ringtone).isNotNull(); assertThat(ringtone.isUsingRemotePlayer()).isFalse(); verify(mMockVibrator).hasVibrator(); // Verify all the properties. assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); assertThat(ringtone.getUri()).isNull(); assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); // Play ringtone.play(); verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); // Set looping doesn't affect an already-started vibration. ringtone.setLooping(true); // This is ignored because there's a vibration effect being used and no sound. ringtone.setHapticGeneratorEnabled(true); // Release ringtone.stop(); 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. verifyZeroInteractions(mMockRemotePlayer); verifyNoMoreInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels() throws Exception { // A media player will still be created for vibration-only because the vibration can come // from haptic channels on the sound file (although in this case it doesn't). MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); when(mMockVibrator.hasVibrator()).thenReturn(true); Ringtone ringtone = newBuilder(MEDIA_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_VIBRATION); assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); // Prepare // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it // knows there aren't any. verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); verify(mockMediaPlayer).release(); // abandoned: no haptic channels. // Play ringtone.play(); verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); // Set looping doesn't affect an already-started vibration. ringtone.setLooping(true); // This is ignored because there's a vibration effect being used and no sound. ringtone.setHapticGeneratorEnabled(true); // Release ringtone.stop(); 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. verifyZeroInteractions(mMockRemotePlayer); verifyNoMoreInteractions(mMockVibrator); verifyNoMoreInteractions(mockMediaPlayer); } @Test public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels() throws Exception { MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); when(mMockVibrator.hasVibrator()).thenReturn(true); mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); Ringtone ringtone = newBuilder(MEDIA_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_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(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); // Vibrator.vibrate isn't called because the vibration comes from the sound. verifyLocalPlay(mockMediaPlayer); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original ringtone.setLooping(true); verify(mockMediaPlayer).isLooping(); verify(mockMediaPlayer).setLooping(true); // This is ignored because it's using haptic channels. ringtone.setHapticGeneratorEnabled(true); // 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. verifyZeroInteractions(mMockRemotePlayer); verifyZeroInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception { MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); Loading Loading
media/java/android/media/LocalRingtonePlayer.java +135 −55 Original line number Diff line number Diff line Loading @@ -39,27 +39,23 @@ 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> sActiveMediaPlayers = new ArrayList<>(); private final MediaPlayer mMediaPlayer; private final AudioAttributes mAudioAttributes; private final VibrationAttributes mVibrationAttributes; private final Ringtone.RingtonePlayer mVibrationPlayer; 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, @NonNull Vibrator vibrator, @Nullable VibrationEffect vibrationEffect) { @Nullable VolumeShaper volumeShaper, @Nullable Ringtone.RingtonePlayer vibrationPlayer) { Objects.requireNonNull(mediaPlayer); Objects.requireNonNull(audioAttributes); Objects.requireNonNull(injectables); Loading @@ -69,11 +65,8 @@ public class LocalRingtonePlayer mInjectables = injectables; mAudioManager = audioManager; mVolumeShaper = volumeShaper; mVibrator = vibrator; mVibrationEffect = vibrationEffect; mVibrationPlayer = vibrationPlayer; mHapticGenerator = hapticGenerator; mVibrationAttributes = (mVibrationEffect == null) ? null : new VibrationAttributes.Builder(audioAttributes).build(); } /** Loading @@ -81,9 +74,11 @@ public class LocalRingtonePlayer * loaded in the local player. */ @Nullable static LocalRingtonePlayer create(@NonNull Context context, @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri, static Ringtone.RingtonePlayer create(@NonNull Context context, @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri, @NonNull AudioAttributes audioAttributes, boolean isVibrationOnly, @Nullable VibrationEffect vibrationEffect, @NonNull Ringtone.Injectables injectables, @Nullable VolumeShaper.Configuration volumeShaperConfig, Loading @@ -100,7 +95,7 @@ public class LocalRingtonePlayer mediaPlayer.setAudioAttributes(audioAttributes); mediaPlayer.setPreferredDevice(preferredDevice); mediaPlayer.setLooping(initialLooping); mediaPlayer.setVolume(initialVolume); mediaPlayer.setVolume(isVibrationOnly ? 0 : initialVolume); if (initialHapticGeneratorEnabled) { hapticGenerator = injectables.createHapticGenerator(mediaPlayer); if (hapticGenerator != null) { Loading @@ -121,8 +116,16 @@ public class LocalRingtonePlayer vibrationEffect = null; } } VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : new VibrationEffectPlayer( vibrationEffect, audioAttributes, vibrator, initialLooping); if (isVibrationOnly && vibrationEffectPlayer != null) { // Abandon the media player now that it's confirmed to not have haptic channels. mediaPlayer.release(); return vibrationEffectPlayer; } return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, hapticGenerator, volumeShaper, vibrator, vibrationEffect); hapticGenerator, volumeShaper, vibrationEffectPlayer); } catch (SecurityException | IOException e) { if (hapticGenerator != null) { hapticGenerator.release(); Loading Loading @@ -179,8 +182,11 @@ public class LocalRingtonePlayer vibrationEffect = null; } } VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : new VibrationEffectPlayer( vibrationEffect, audioAttributes, vibrator, initialLooping); return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, /* hapticGenerator= */ null, volumeShaper, vibrator, vibrationEffect); /* hapticGenerator= */ null, volumeShaper, vibrationEffectPlayer); } catch (SecurityException | IOException e) { Log.e(TAG, "Failed to open fallback ringtone"); // TODO: vibration-effect-only / no-sound LocalRingtonePlayer. Loading Loading @@ -217,33 +223,8 @@ public class LocalRingtonePlayer } 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); } if (mVibrationPlayer != null) { mVibrationPlayer.play(); } } Loading @@ -260,7 +241,13 @@ public class LocalRingtonePlayer try { mMediaPlayer.stop(); } finally { stopVibration(); // Won't throw: catches exceptions. if (mVibrationPlayer != null) { try { mVibrationPlayer.stopAndRelease(); } catch (Exception e) { Log.e(TAG, "Exception stopping ringtone vibration", e); } } if (mHapticGenerator != null) { mHapticGenerator.release(); } Loading @@ -282,25 +269,19 @@ public class LocalRingtonePlayer 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(); } if (mVibrationPlayer != null) { mVibrationPlayer.setLooping(looping); } } @Override public void setHapticGeneratorEnabled(boolean enabled) { if (mVibrationEffect != null) { // Ignore haptic generator changes if a vibration effect is present. The decision to if (mVibrationPlayer != null) { // Ignore haptic generator changes if a vibration player is present. The decision to // use one or the other happens before this object is constructed. return; } if (enabled && mHapticGenerator == null) { if (enabled && mHapticGenerator == null && !hasHapticChannels()) { mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer); } if (mHapticGenerator != null) { Loading @@ -311,6 +292,7 @@ public class LocalRingtonePlayer @Override public void setVolume(float volume) { mMediaPlayer.setVolume(volume); // no effect on vibration player } @Override Loading @@ -324,5 +306,103 @@ public class LocalRingtonePlayer sActiveMediaPlayers.remove(this); } mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. // No effect on vibration: either it's looping and this callback only happens when stopped, // or it's not looping, in which case the vibration should play to its own completion. } /** A RingtonePlayer that only plays a VibrationEffect. */ static class VibrationEffectPlayer implements Ringtone.RingtonePlayer { private static final int VIBRATION_LOOP_DELAY_MS = 200; private final VibrationEffect mVibrationEffect; private final VibrationAttributes mVibrationAttributes; private final Vibrator mVibrator; private boolean mIsLooping; private boolean mStartedVibration; VibrationEffectPlayer(@NonNull VibrationEffect vibrationEffect, @NonNull AudioAttributes audioAttributes, @NonNull Vibrator vibrator, boolean initialLooping) { mVibrationEffect = vibrationEffect; mVibrationAttributes = new VibrationAttributes.Builder(audioAttributes).build(); mVibrator = vibrator; mIsLooping = initialLooping; } @Override public boolean play() { if (!mStartedVibration) { try { // Adjust the vibration effect to loop. VibrationEffect loopAdjustedEffect = mVibrationEffect.applyRepeatingIndefinitely( mIsLooping, 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 " + (mIsLooping ? "looping " : "") + "vibration " + "for ringtone: " + mVibrationEffect, e); return false; } } return true; } @Override public boolean isPlaying() { return mStartedVibration; } @Override public void stopAndRelease() { if (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 void setPreferredDevice(AudioDeviceInfo audioDeviceInfo) { // no-op } @Override public void setLooping(boolean looping) { if (looping == mIsLooping) { return; } mIsLooping = looping; if (mStartedVibration) { if (!mIsLooping) { // Was looping, stop looping stopAndRelease(); } // Else was not looping, but can't interfere with a running vibration without // restarting it, and don't know if it was finished. So do nothing: apps shouldn't // toggle looping after calling play anyway. } } @Override public void setHapticGeneratorEnabled(boolean enabled) { // n/a } @Override public void setVolume(float volume) { // n/a } @Override public boolean hasHapticChannels() { return false; } } }
media/java/android/media/Ringtone.java +21 −2 Original line number Diff line number Diff line Loading @@ -286,14 +286,29 @@ public class Ringtone { stopAndReleaseActivePlayer(); } boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION; // Vibration can come from the audio file if using haptic generator or if haptic // channels are a possibility. boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported() && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted()); // VibrationEffect only, use the simplified player without checking for haptic channels. if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) { mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer( mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping); return true; } AudioDeviceInfo preferredDevice = mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null; if (mUri != null) { mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri, mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping, mVolume); mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables, mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping, mVolume); } else { // Using the remote player won't help play a null Uri. Revert straight to fallback. // The vibration-only case was already covered above. mActivePlayer = createFallbackRingtonePlayer(); // Fall through to attempting remote fallback play if null. } Loading Loading @@ -388,6 +403,10 @@ public class Ringtone { * corresponds to no attenuation being applied. */ public void setVolume(float volume) { // Ignore if sound not enabled. if ((mEnabledMedia & MEDIA_SOUND) == 0) { return; } if (volume < 0.0f) { volume = 0.0f; } else if (volume > 1.0f) { Loading
media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java +156 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.mediaframeworktest.unit; import static android.media.Ringtone.MEDIA_SOUND; import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION; import static android.media.Ringtone.MEDIA_VIBRATION; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; Loading @@ -31,7 +32,6 @@ 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; Loading Loading @@ -293,7 +293,6 @@ public class RingtoneTest { ringtone.play(); verifyLocalPlay(mockMediaPlayer); verify(mockMediaPlayer).isLooping(); // When starting the vibration. verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls. Loading @@ -303,8 +302,7 @@ public class RingtoneTest { // 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).isLooping(); verify(mockMediaPlayer).setLooping(true); // This is ignored because there's a vibration effect being used. Loading @@ -323,6 +321,160 @@ public class RingtoneTest { verifyNoMoreInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationOnly() throws Exception { when(mMockVibrator.hasVibrator()).thenReturn(true); Ringtone ringtone = newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) // TODO: set sound uri too in diff test .setVibrationEffect(VIBRATION_EFFECT) .build(); assertThat(ringtone).isNotNull(); assertThat(ringtone.isUsingRemotePlayer()).isFalse(); verify(mMockVibrator).hasVibrator(); // Verify all the properties. assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); assertThat(ringtone.getUri()).isNull(); assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); // Play ringtone.play(); verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); // Set looping doesn't affect an already-started vibration. ringtone.setLooping(true); // This is ignored because there's a vibration effect being used and no sound. ringtone.setHapticGeneratorEnabled(true); // Release ringtone.stop(); 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. verifyZeroInteractions(mMockRemotePlayer); verifyNoMoreInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels() throws Exception { // A media player will still be created for vibration-only because the vibration can come // from haptic channels on the sound file (although in this case it doesn't). MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); when(mMockVibrator.hasVibrator()).thenReturn(true); Ringtone ringtone = newBuilder(MEDIA_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_VIBRATION); assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); // Prepare // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it // knows there aren't any. verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); verify(mockMediaPlayer).release(); // abandoned: no haptic channels. // Play ringtone.play(); verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); // Set looping doesn't affect an already-started vibration. ringtone.setLooping(true); // This is ignored because there's a vibration effect being used and no sound. ringtone.setHapticGeneratorEnabled(true); // Release ringtone.stop(); 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. verifyZeroInteractions(mMockRemotePlayer); verifyNoMoreInteractions(mMockVibrator); verifyNoMoreInteractions(mockMediaPlayer); } @Test public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels() throws Exception { MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); when(mMockVibrator.hasVibrator()).thenReturn(true); mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); Ringtone ringtone = newBuilder(MEDIA_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_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(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); // Vibrator.vibrate isn't called because the vibration comes from the sound. verifyLocalPlay(mockMediaPlayer); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original ringtone.setLooping(true); verify(mockMediaPlayer).isLooping(); verify(mockMediaPlayer).setLooping(true); // This is ignored because it's using haptic channels. ringtone.setHapticGeneratorEnabled(true); // 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. verifyZeroInteractions(mMockRemotePlayer); verifyZeroInteractions(mMockVibrator); } @Test public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception { MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); Loading