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

Commit 0587707b authored by Tyler Gunn's avatar Tyler Gunn Committed by android-build-merger
Browse files

Support for audio coupled haptics when playing ringtones.

am: 8c47a677

Change-Id: Ica565e7325f12e2ab1a7a280c779ceff7d578198
parents e5bc9e0f 8c47a677
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.