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

Commit 8c47a677 authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Support for audio coupled haptics when playing ringtones.

Updated Ringer.java to suppress system haptics when a ringtone which will
be played has embedded haptics and the device supports audio coupled
haptics.

AsyncRingtonePlayer performs all of its operations on a Handler to avoid
possible strict mode violations when manipulating ringtones, so the play
now returns a completable future which indicates whether audio coupled
haptics will be used to play a ringtone.  Ringer.java waits on this future
and continues with the business of playing the system haptics if needed.

Also updated the RingerTest code because changes made to support ramping
ringtones effectively nullified most of the tests.

Test: Manual, added unit tests
Bug: 127818951
Change-Id: Ie5fc285a0181b27672fd4f8ffae9610b1b1f3b02
Merged-In: Ie5fc285a0181b27672fd4f8ffae9610b1b1f3b02
(cherry picked from commit 9c4ebe80)
parent b1854ccc
Loading
Loading
Loading
Loading
+61 −9
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.server.telecom;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.VolumeShaper;
import android.net.Uri;
@@ -29,6 +31,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;

import java.util.concurrent.CompletableFuture;

/**
 * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
 * used from the main thread.
@@ -49,6 +53,12 @@ public class AsyncRingtonePlayer {
    /** The current ringtone. Only used by the ringtone thread. */
    private Ringtone mRingtone;

    /**
     * CompletableFuture which signals a caller when we know whether a ringtone will play haptics
     * or not.
     */
    private CompletableFuture<Boolean> mHapticsFuture = null;

    /**
     * Determines if the {@link AsyncRingtonePlayer} should pause between repeats of the ringtone.
     * When {@code true}, the system will check if the ringtone has stopped every
@@ -69,20 +79,36 @@ public class AsyncRingtonePlayer {
        mShouldPauseBetweenRepeat = shouldPauseBetweenRepeat;
    }

    /** Plays the ringtone with ramping ringer if required. */
    public void play(RingtoneFactory factory, Call incomingCall,
            @Nullable VolumeShaper.Configuration volumeShaperConfig) {
    /**
     * Plays the appropriate ringtone for the specified call.
     * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
     * the volume of the ringtone as it plays.
     *
     * @param factory The {@link RingtoneFactory}.
     * @param incomingCall The ringing {@link Call}.
     * @param volumeShaperConfig An optional {@link VolumeShaper.Configuration} which is applied to
     *                           the ringtone to change its volume while it rings.
     * @param isVibrationEnabled {@code true} if the settings and DND configuration of the device
     *                           is such that the vibrator should be used, {@code false} otherwise.
     * @return A {@link CompletableFuture} which on completion indicates whether or not the ringtone
     *         has a haptic track.  {@code True} indicates that a haptic track is present on the
     *         ringtone; in this case the default vibration in {@link Ringer} should not be played.
     *         {@code False} indicates that a haptic track is NOT present on the ringtone;
     *         in this case the default vibration in {@link Ringer} should be trigger if needed.
     */
    public @NonNull CompletableFuture<Boolean> play(RingtoneFactory factory, Call incomingCall,
            @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean isVibrationEnabled) {
        Log.d(this, "Posting play.");
        if (mHapticsFuture == null) {
            mHapticsFuture = new CompletableFuture<>();
        }
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = factory;
        args.arg2 = incomingCall;
        args.arg3 = volumeShaperConfig;
        args.arg4 = isVibrationEnabled;
        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
    }

    /** Plays the ringtone. */
    public void play(RingtoneFactory factory, Call incomingCall) {
        play(factory, incomingCall, null);
        return mHapticsFuture;
    }

    /** Stops playing the ringtone. */
@@ -146,9 +172,12 @@ public class AsyncRingtonePlayer {
        RingtoneFactory factory = (RingtoneFactory) args.arg1;
        Call incomingCall = (Call) args.arg2;
        VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3;
        boolean isVibrationEnabled = (boolean) args.arg4;
        args.recycle();
        // don't bother with any of this if there is an EVENT_STOP waiting.
        if (mHandler.hasMessages(EVENT_STOP)) {
            mHapticsFuture.complete(false /* ringtoneHasHaptics */);
            mHapticsFuture = null;
            return;
        }

@@ -156,11 +185,13 @@ public class AsyncRingtonePlayer {
        // anything.
        if(Uri.EMPTY.equals(incomingCall.getRingtone())) {
            mRingtone = null;
            mHapticsFuture.complete(false /* ringtoneHasHaptics */);
            mHapticsFuture = null;
            return;
        }

        ThreadUtil.checkNotOnMainThread();
        Log.i(this, "Play ringtone.");
        Log.i(this, "handlePlay: Play ringtone.");

        if (mRingtone == null) {
            mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig);
@@ -170,8 +201,29 @@ public class AsyncRingtonePlayer {
                        ringtoneUri.toSafeString();
                Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " +
                        "factory. Skipping ringing. Uri was: " + ringtoneUriString);
                mHapticsFuture.complete(false /* ringtoneHasHaptics */);
                mHapticsFuture = null;
                return;
            }

            // With the ringtone to play now known, we can determine if it has haptic channels or
            // not; we will complete the haptics future so the default vibration code in Ringer
            // can know whether to trigger the vibrator.
            if (mHapticsFuture != null && !mHapticsFuture.isDone()) {
                boolean hasHaptics = factory.hasHapticChannels(mRingtone);

                Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics,
                        isVibrationEnabled);
                if (!isVibrationEnabled && hasHaptics) {
                    Log.i(this, "handlePlay: muting haptic channel");
                    mRingtone.setAudioAttributes(
                            new AudioAttributes.Builder()
                                .setMuteHapticChannels(true)
                                .build());
                }
                mHapticsFuture.complete(hasHaptics);
                mHapticsFuture = null;
            }
        }

        if (mShouldPauseBetweenRepeat) {
+68 −11
Original line number Diff line number Diff line
@@ -32,11 +32,12 @@ import android.os.Bundle;
import android.os.Vibrator;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

/**
@@ -138,6 +139,12 @@ public class Ringer {
    private final Vibrator mVibrator;
    private final InCallController mInCallController;
    private final VibrationEffectProxy mVibrationEffectProxy;
    private final boolean mIsHapticPlaybackSupportedByDevice;
    /**
     * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
     * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
     */
    private CompletableFuture<Void> mBlockOnRingingFuture = null;

    private InCallTonePlayer mCallWaitingPlayer;
    private RingtoneFactory mRingtoneFactory;
@@ -185,6 +192,14 @@ public class Ringer {
            mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN,
                    PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
        }

        mIsHapticPlaybackSupportedByDevice =
                mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
    }

    @VisibleForTesting
    public void setBlockOnRingingFuture(CompletableFuture<Void> future) {
        mBlockOnRingingFuture = future;
    }

    public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
@@ -229,12 +244,18 @@ public class Ringer {
                            "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s",
                    isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger,
                    isSilentRingingRequested);
            if (mBlockOnRingingFuture != null) {
                mBlockOnRingingFuture.complete(null);
            }
            return shouldAcquireAudioFocus;
        }

        stopCallWaiting();

        VibrationEffect effect;
        CompletableFuture<Boolean> hapticsFuture = null;
        // Determine if the settings and DND mode indicate that the vibrator can be used right now.
        boolean isVibratorEnabled = isVibratorEnabled(mContext, foregroundCall);
        if (isRingerAudible) {
            mRingingCall = foregroundCall;
            Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
@@ -277,9 +298,12 @@ public class Ringer {
                        .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
                        .build();
                }
                mRingtonePlayer.play(mRingtoneFactory, foregroundCall, mVolumeShaperConfig);
                hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall,
                        mVolumeShaperConfig, isVibratorEnabled);
            } else {
                mRingtonePlayer.play(mRingtoneFactory, foregroundCall, null);
                // Ramping ringtone is not enabled.
                hapticsFuture = mRingtonePlayer.play(mRingtoneFactory, foregroundCall, null,
                        isVibratorEnabled);
                effect = getVibrationEffectForCall(mRingtoneFactory, foregroundCall);
            }
        } else {
@@ -291,23 +315,56 @@ public class Ringer {
            effect = mDefaultVibrationEffect;
        }

        if (shouldVibrate(mContext, foregroundCall)
        if (hapticsFuture != null) {
            CompletableFuture<Void> vibrateFuture =
                    hapticsFuture.thenAccept(isUsingAudioCoupledHaptics -> {
                if (!isUsingAudioCoupledHaptics || !mIsHapticPlaybackSupportedByDevice) {
                    Log.i(this, "startRinging: fileHasHaptics=%b, hapticsSupported=%b",
                            isUsingAudioCoupledHaptics, mIsHapticPlaybackSupportedByDevice);
                    maybeStartVibration(foregroundCall, shouldRingForContact, effect,
                            isVibratorEnabled, isRingerAudible);
                } else {
                    Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
                            "using audio-coupled haptics");
                }
            });
            if (mBlockOnRingingFuture != null) {
                vibrateFuture.thenCompose( v -> {
                    mBlockOnRingingFuture.complete(null);
                    return null;
                });
            }
        } else {
            if (mBlockOnRingingFuture != null) {
                mBlockOnRingingFuture.complete(null);
            }
            Log.w(this, "startRinging: No haptics future; fallback to default behavior");
            maybeStartVibration(foregroundCall, shouldRingForContact, effect, isVibratorEnabled,
                    isRingerAudible);
        }

        return shouldAcquireAudioFocus;
    }

    private void maybeStartVibration(Call foregroundCall, boolean shouldRingForContact,
        VibrationEffect effect, boolean isVibrationEnabled, boolean isRingerAudible) {

        if (isVibrationEnabled
                && !mIsVibrating && shouldRingForContact) {
            if (mSystemSettingsUtil.applyRampingRinger(mContext)
                    && mSystemSettingsUtil.enableRampingRingerFromDeviceConfig()
                    && isRingerAudible) {
                Log.i(this, "start vibration for ramping ringer.");
                mVibrator.vibrate(effect);
                mIsVibrating = true;
            } else {
                Log.i(this, "start normal vibration.");
                mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
            }
                mIsVibrating = true;
            }
        } else if (mIsVibrating) {
            Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "already vibrating");
        }

        return shouldAcquireAudioFocus;
    }

    private VibrationEffect createRampingRingerVibrationEffect(int vibrationSeconds) {
@@ -443,7 +500,7 @@ public class Ringer {
        }
    }

    private boolean shouldVibrate(Context context, Call call) {
    private boolean isVibratorEnabled(Context context, Call call) {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        int ringerMode = audioManager.getRingerModeInternal();
        boolean shouldVibrate;
+11 −0
Original line number Diff line number Diff line
@@ -54,6 +54,17 @@ public class RingtoneFactory {
        mCallsManager = callsManager;
    }

    /**
     * Determines if a ringtone has haptic channels.
     * @param ringtone The ringtone URI.
     * @return {@code true} if there is a haptic channel, {@code false} otherwise.
     */
    public boolean hasHapticChannels(Ringtone ringtone) {
        boolean hasHapticChannels = RingtoneManager.hasHapticChannels(ringtone.getUri());
        Log.i(this, "hasHapticChannels %s -> %b", ringtone.getUri(), hasHapticChannels);
        return hasHapticChannels;
    }

    public Ringtone getRingtone(Call incomingCall,
            @Nullable VolumeShaper.Configuration volumeShaperConfig) {
        // Use the default ringtone of the work profile if the contact is a work profile contact.
+5 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.telecom;

import android.content.Context;
import android.media.AudioManager;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.telecom.Log;
@@ -78,5 +79,9 @@ public class SystemSettingsUtil {
        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TELEPHONY, 
                RAMPING_RINGER_VIBRATION_DURATION, 0);
    }

    public boolean isHapticPlaybackSupported(Context context) {
        return context.getSystemService(AudioManager.class).isHapticPlaybackSupported();
    }
}
+88 −22

File changed.

Preview size limit exceeded, changes collapsed.