Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f8942d91 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add support for VibrationEffect and audio-coupled vibrations to Ringtone."

parents e91e74c5 108385e1
Loading
Loading
Loading
Loading
+26 −18
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.Log;
import android.util.MathUtils;

import com.android.internal.util.Preconditions;
@@ -52,6 +53,7 @@ import java.util.Objects;
 * <p>These effects may be any number of things, from single shot vibrations to complex waveforms.
 */
public abstract class VibrationEffect implements Parcelable {
    private static final String TAG = "VibrationEffect";
    // Stevens' coefficient to scale the perceived vibration intensity.
    private static final float SCALE_GAMMA = 0.65f;
    // If a vibration is playing for longer than 1s, it's probably not haptic feedback
@@ -394,12 +396,13 @@ public abstract class VibrationEffect implements Parcelable {
            return null;
        }

        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.
                // this case, just use the URI as passed in since it wasn't canonicalized in the
                // first place.
                uncanonicalUri = uri;
            }

@@ -415,6 +418,11 @@ public abstract class VibrationEffect implements Parcelable {
                    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;
    }

+3 −1
Original line number Diff line number Diff line
@@ -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);
+113 −35
Original line number Diff line number Diff line
@@ -23,6 +23,9 @@ import android.content.res.AssetFileDescriptor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
import android.os.Trace;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;

import java.io.IOException;
@@ -36,21 +39,27 @@ import java.util.Objects;
public class LocalRingtonePlayer
        implements Ringtone.RingtonePlayer, MediaPlayer.OnCompletionListener {
    private static final String TAG = "LocalRingtonePlayer";
    private static final int VIBRATION_LOOP_DELAY_MS = 200;

    // keep references on active Ringtones until stopped or completion listener called.
    private static final ArrayList<LocalRingtonePlayer> sActiveRingtones = new ArrayList<>();
    private static final ArrayList<LocalRingtonePlayer> sActiveMediaPlayers = new ArrayList<>();

    private final MediaPlayer mMediaPlayer;
    private final AudioAttributes mAudioAttributes;
    private final VibrationAttributes mVibrationAttributes;
    private final Ringtone.Injectables mInjectables;
    private final AudioManager mAudioManager;
    private final VolumeShaper mVolumeShaper;
    private final Vibrator mVibrator;
    private final VibrationEffect mVibrationEffect;
    private HapticGenerator mHapticGenerator;
    private boolean mStartedVibration;

    private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer,
            @NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables,
            @NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator,
            @Nullable VolumeShaper volumeShaper) {
            @Nullable VolumeShaper volumeShaper, @NonNull Vibrator vibrator,
            @Nullable VibrationEffect vibrationEffect) {
        Objects.requireNonNull(mediaPlayer);
        Objects.requireNonNull(audioAttributes);
        Objects.requireNonNull(injectables);
@@ -60,7 +69,11 @@ public class LocalRingtonePlayer
        mInjectables = injectables;
        mAudioManager = audioManager;
        mVolumeShaper = volumeShaper;
        mVibrator = vibrator;
        mVibrationEffect = vibrationEffect;
        mHapticGenerator = hapticGenerator;
        mVibrationAttributes = (mVibrationEffect == null) ? null :
                new VibrationAttributes.Builder(audioAttributes).build();
    }

    /**
@@ -69,8 +82,9 @@ public class LocalRingtonePlayer
     */
    @Nullable
    static LocalRingtonePlayer create(@NonNull Context context,
            @NonNull AudioManager audioManager, @NonNull Uri soundUri,
            @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, @NonNull Uri soundUri,
            @NonNull AudioAttributes audioAttributes,
            @Nullable VibrationEffect vibrationEffect,
            @NonNull Ringtone.Injectables injectables,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            @Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled,
@@ -89,15 +103,26 @@ public class LocalRingtonePlayer
            mediaPlayer.setVolume(initialVolume);
            if (initialHapticGeneratorEnabled) {
                hapticGenerator = injectables.createHapticGenerator(mediaPlayer);
                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();
            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);
                    /* 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,9 +254,13 @@ public class LocalRingtonePlayer

    @Override
    public void stopAndRelease() {
        synchronized (sActiveRingtones) {
            sActiveRingtones.remove(this);
        synchronized (sActiveMediaPlayers) {
            sActiveMediaPlayers.remove(this);
        }
        try {
            mMediaPlayer.stop();
        } finally {
            stopVibration();  // Won't throw: catches exceptions.
            if (mHapticGenerator != null) {
                mHapticGenerator.release();
            }
@@ -194,6 +268,7 @@ public class LocalRingtonePlayer
            mMediaPlayer.reset();
            mMediaPlayer.release();
        }
    }

    @Override
    public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
@@ -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.
    }
+221 −33

File changed.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -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();
    }

Loading