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

Commit e0542145 authored by Simon Bowden's avatar Simon Bowden
Browse files

Move Ringtone creation onto the AsyncRingtonePlayer thread.

Ringtone initialization on the main thread can cause deadlocks, because
it internally blocks on some asynchronous tasks.

Use the real AsyncRingtonePlayer in the RingerTest.

Bug: 273629038
Test: atest, presubmit
Change-Id: Ifc9f19d0a847fc12e75d795ae7fd3ed3b1729382
parent ed96db50
Loading
Loading
Loading
Loading
+40 −22
Original line number Original line Diff line number Diff line
@@ -18,9 +18,7 @@ package com.android.server.telecom;


import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.media.VolumeShaper;
import android.media.VolumeShaper;
import android.net.Uri;
import android.net.Uri;
import android.os.Handler;
import android.os.Handler;
@@ -32,6 +30,9 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
import com.android.internal.util.Preconditions;


import java.util.function.BiConsumer;
import java.util.function.Supplier;

/**
/**
 * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
 * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
 * used from the main thread.
 * used from the main thread.
@@ -57,13 +58,16 @@ public class AsyncRingtonePlayer {
     * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
     * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
     * the volume of the ringtone as it plays.
     * the volume of the ringtone as it plays.
     *
     *
     * @param ringtone The {@link Ringtone}.
     * @param ringtoneSupplier The {@link Ringtone} factory.
     * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
     */
     */
    public void play(@NonNull Ringtone ringtone) {
    public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
            BiConsumer<Ringtone, Boolean> ringtoneConsumer) {
        Log.d(this, "Posting play.");
        Log.d(this, "Posting play.");
        SomeArgs args = SomeArgs.obtain();
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = ringtone;
        args.arg1 = ringtoneSupplier;
        args.arg2 = Log.createSubsession();
        args.arg2 = ringtoneConsumer;
        args.arg3 = Log.createSubsession();
        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
    }
    }


@@ -122,20 +126,31 @@ public class AsyncRingtonePlayer {
     * Starts the actual playback of the ringtone. Executes on ringtone-thread.
     * Starts the actual playback of the ringtone. Executes on ringtone-thread.
     */
     */
    private void handlePlay(SomeArgs args) {
    private void handlePlay(SomeArgs args) {
        Ringtone ringtone = (Ringtone) args.arg1;
        Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
        Session session = (Session) args.arg2;
        BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
        Session session = (Session) args.arg3;
        args.recycle();
        args.recycle();


        Log.continueSession(session, "ARP.hP");
        Log.continueSession(session, "ARP.hP");
        try {
        try {
            // don't bother with any of this if there is an EVENT_STOP waiting.
            // Don't bother with any of this if there is an EVENT_STOP waiting, but give the
            // consumer a chance to do anything no matter what.
            if (mHandler.hasMessages(EVENT_STOP)) {
            if (mHandler.hasMessages(EVENT_STOP)) {
                ringtoneConsumer.accept(null, /* stopped= */ true);
                return;
                return;
            }
            }
            Ringtone ringtone = null;
            try {
                ringtone = ringtoneSupplier.get();


            ThreadUtil.checkNotOnMainThread();
                // setRingtone even if null - it also stops any current ringtone to be consistent

                // with the overall state.
                setRingtone(ringtone);
                setRingtone(ringtone);
                if (mRingtone == null) {
                    // The ringtoneConsumer can still vibrate at this stage.
                    Log.w(this, "No ringtone was found bail out from playing.");
                    return;
                }
                Uri uri = mRingtone.getUri();
                Uri uri = mRingtone.getUri();
                String uriString = (uri != null ? uri.toSafeString() : "");
                String uriString = (uri != null ? uri.toSafeString() : "");
                Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
                Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
@@ -146,6 +161,9 @@ public class AsyncRingtonePlayer {
                }
                }
                mRingtone.play();
                mRingtone.play();
                Log.i(this, "Play ringtone, looping.");
                Log.i(this, "Play ringtone, looping.");
            } finally {
                ringtoneConsumer.accept(ringtone, /* stopped= */ false);
            }
        } finally {
        } finally {
            Log.cancelSubsession(session);
            Log.cancelSubsession(session);
        }
        }
+203 −185
Original line number Original line Diff line number Diff line
@@ -51,6 +51,8 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Supplier;


/**
/**
 * Controls the ringtone player.
 * Controls the ringtone player.
@@ -246,6 +248,9 @@ public class Ringer {
    }
    }


    public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
    public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {
        boolean deferBlockOnRingingFuture = false;
        // try-finally to ensure that the block on ringing future is always called.
        try {
            if (foregroundCall == null) {
            if (foregroundCall == null) {
                Log.wtf(this, "startRinging called with null foreground call.");
                Log.wtf(this, "startRinging called with null foreground call.");
                return false;
                return false;
@@ -253,16 +258,16 @@ public class Ringer {


            if (foregroundCall.getState() != CallState.RINGING
            if (foregroundCall.getState() != CallState.RINGING
                    && foregroundCall.getState() != CallState.SIMULATED_RINGING) {
                    && foregroundCall.getState() != CallState.SIMULATED_RINGING) {
            // Its possible for bluetooth to connect JUST as a call goes active, which would mean
                // It's possible for bluetooth to connect JUST as a call goes active, which would
            // the call would start ringing again.
                // mean the call would start ringing again.
                Log.i(this, "startRinging called for non-ringing foreground callid=%s",
                Log.i(this, "startRinging called for non-ringing foreground callid=%s",
                        foregroundCall.getId());
                        foregroundCall.getId());
                return false;
                return false;
            }
            }


        // Use completable future to establish a timeout, not intent to make these work outside the
            // Use completable future to establish a timeout, not intent to make these work outside
        // main thread asynchronously
            // the main thread asynchronously
        // TODO: moving these RingerAttributes calculation out of Telecom lock to avoid blocking.
            // TODO: moving these RingerAttributes calculation out of Telecom lock to avoid blocking
            CompletableFuture<RingerAttributes> ringerAttributesFuture = CompletableFuture
            CompletableFuture<RingerAttributes> ringerAttributesFuture = CompletableFuture
                    .supplyAsync(() -> getRingerAttributes(foregroundCall, isHfpDeviceAttached),
                    .supplyAsync(() -> getRingerAttributes(foregroundCall, isHfpDeviceAttached),
                            new LoggedHandlerExecutor(getHandler(), "R.sR", null));
                            new LoggedHandlerExecutor(getHandler(), "R.sR", null));
@@ -278,7 +283,8 @@ public class Ringer {
            }
            }


            if (attributes == null) {
            if (attributes == null) {
            Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "RingerAttributes error");
                Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
                        "RingerAttributes error");
                return false;
                return false;
            }
            }


@@ -298,9 +304,6 @@ public class Ringer {
                    Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
                    Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,
                            "Work profile in quiet mode");
                            "Work profile in quiet mode");
                }
                }
            if (mBlockOnRingingFuture != null) {
                mBlockOnRingingFuture.complete(null);
            }
                return acquireAudioFocus;
                return acquireAudioFocus;
            }
            }


@@ -314,7 +317,8 @@ public class Ringer {
                                AccessibilityManager.FLASH_REASON_CALL));
                                AccessibilityManager.FLASH_REASON_CALL));
            }
            }


        // Determine if the settings and DND mode indicate that the vibrator can be used right now.
            // Determine if the settings and DND mode indicate that the vibrator can be used right
            // now.
            final boolean isVibratorEnabled =
            final boolean isVibratorEnabled =
                    isVibratorEnabled(mContext, attributes.shouldRingForContact());
                    isVibratorEnabled(mContext, attributes.shouldRingForContact());
            boolean shouldApplyRampingRinger =
            boolean shouldApplyRampingRinger =
@@ -338,11 +342,14 @@ public class Ringer {
                            / (float) (RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION);
                            / (float) (RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION);
                    mVolumeShaperConfig =
                    mVolumeShaperConfig =
                            new VolumeShaper.Configuration.Builder()
                            new VolumeShaper.Configuration.Builder()
                        .setDuration(RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION)
                                    .setDuration(RAMPING_RINGER_VIBRATION_DURATION
                                            + RAMPING_RINGER_DURATION)
                                    .setCurve(
                                    .setCurve(
                            new float[] {0.f, silencePoint + EPSILON /*keep monotonicity*/, 1.f},
                                            new float[]{0.f, silencePoint + EPSILON
                                                    /*keep monotonicity*/, 1.f},
                                            new float[]{0.f, 0.f, 1.f})
                                            new float[]{0.f, 0.f, 1.f})
                        .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
                                    .setInterpolatorType(
                                            VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
                                    .build();
                                    .build();
                    if (mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {
                    if (mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {
                        useCustomVibrationEffect = true;
                        useCustomVibrationEffect = true;
@@ -367,9 +374,6 @@ public class Ringer {
                        Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);
                        Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);
                    }
                    }
                } else {
                } else {
                if (mBlockOnRingingFuture != null) {
                  mBlockOnRingingFuture.complete(null);
                }
                    foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
                    foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
                    return attributes.shouldAcquireAudioFocus(); // ringer not audible
                    return attributes.shouldAcquireAudioFocus(); // ringer not audible
                }
                }
@@ -382,24 +386,40 @@ public class Ringer {
                Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
                Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
                hapticChannelsMuted = true;
                hapticChannelsMuted = true;
            } else if (hapticChannelsMuted) {
            } else if (hapticChannelsMuted) {
            Log.i(this, "Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
                Log.i(this,
                        "Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
                        isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
                        isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
            }
            }
        Ringtone ringtone;
            // Defer ringtone creation to the async player thread.
            Supplier<Ringtone> ringtoneSupplier;
            final boolean finalHapticChannelsMuted = hapticChannelsMuted;
            if (isHapticOnly) {
            if (isHapticOnly) {
                if (hapticChannelsMuted) {
                if (hapticChannelsMuted) {
                Log.i(this, "want haptic only ringtone but haptics are muted, skip ringtone play");
                    Log.i(this,
                ringtone = null;
                            "want haptic only ringtone but haptics are muted, skip ringtone play");
                    ringtoneSupplier = null;
                } else {
                } else {
                ringtone = mRingtoneFactory.getHapticOnlyRingtone();
                    ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
                }
                }
            } else {
            } else {
            ringtone = mRingtoneFactory.getRingtone(
                ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
                foregroundCall, mVolumeShaperConfig, hapticChannelsMuted);
                        foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
            }

            // The vibration logic depends on the loaded ringtone, but we need to defer the ringtone
            // load to the async ringtone thread. Hence, we bundle up the final part of this method
            // for that thread to run after loading the ringtone. This logic is intended to run even
            // if the loaded ringtone is null. However if a stop event arrives before the ringtone
            // creation finishes, then this consumer can be skipped.
            final boolean finalUseCustomVibrationEffect = useCustomVibrationEffect;
            final RingerAttributes finalAttributes = attributes;
            BiConsumer<Ringtone, Boolean> vibrationLogic = (Ringtone ringtone, Boolean stopped) -> {
                try {
                    if (stopped.booleanValue()) {
                        return;  // don't start vibration if the ringing is already abandoned.
                    }
                    }

                    final VibrationEffect vibrationEffect;
                    final VibrationEffect vibrationEffect;
        if (useCustomVibrationEffect) {
                    if (ringtone != null && finalUseCustomVibrationEffect) {
                        if (DEBUG_RINGER) {
                        if (DEBUG_RINGER) {
                            Log.d(this, "Using ringtone defined vibration effect.");
                            Log.d(this, "Using ringtone defined vibration effect.");
                        }
                        }
@@ -408,31 +428,32 @@ public class Ringer {
                        vibrationEffect = mDefaultVibrationEffect;
                        vibrationEffect = mDefaultVibrationEffect;
                    }
                    }


        if (ringtone == null) {
                    boolean isUsingAudioCoupledHaptics =
            Log.w(this, "No ringtone was found bail out from playing.");
                            !finalHapticChannelsMuted && ringtone != null
            // No ringtone was found, try as a last resort to check if vibration can be performed.
                                    && ringtone.hasHapticChannels();
            vibrateIfNeeded(/* isUsingAudioCoupledHaptics */ false, attributes, foregroundCall,
                    vibrateIfNeeded(isUsingAudioCoupledHaptics, finalAttributes, foregroundCall,
                            vibrationEffect, isVibratorEnabled);
                            vibrationEffect, isVibratorEnabled);
                } finally {
                    // This is used to signal to tests that the async play() call has completed.
                    if (mBlockOnRingingFuture != null) {
                    if (mBlockOnRingingFuture != null) {
                        mBlockOnRingingFuture.complete(null);
                        mBlockOnRingingFuture.complete(null);
                    }
                    }
            return attributes.shouldAcquireAudioFocus();
        }

        boolean isUsingAudioCoupledHaptics = !hapticChannelsMuted && ringtone.hasHapticChannels();

        // There is a ringtone and we want to play it.
        mRingtonePlayer.play(ringtone);

        vibrateIfNeeded(isUsingAudioCoupledHaptics, attributes, foregroundCall, vibrationEffect,
            isVibratorEnabled);
        if (mBlockOnRingingFuture != null) {
            mBlockOnRingingFuture.complete(null);
                }
                }
            };
            deferBlockOnRingingFuture = true;  // Run in vibrationLogic.
            mRingtonePlayer.play(ringtoneSupplier, vibrationLogic);


            // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
            // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
            // because until now is when we actually know if the ringtone loading worked.
            // because until now is when we actually know if the ringtone loading worked.
        return attributes.shouldAcquireAudioFocus() || attributes.isRingerAudible();
            return attributes.shouldAcquireAudioFocus()
                    || (!isHapticOnly && attributes.isRingerAudible());
        } finally {
            // This is used to signal to tests that the async play() call has completed. It can
            // be deferred into AsyncRingtonePlayer
            if (mBlockOnRingingFuture != null && !deferBlockOnRingingFuture) {
                mBlockOnRingingFuture.complete(null);
            }
        }
    }
    }


    private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, RingerAttributes attributes,
    private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, RingerAttributes attributes,
@@ -465,29 +486,26 @@ public class Ringer {
        }
        }
    }
    }


    private VibrationEffect getVibrationEffectForRingtone(Ringtone ringtone) {
    private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
        VibrationEffect effect = null;
        Uri ringtoneUri = ringtone.getUri();
        Uri ringtoneUri = ringtone != null ? ringtone.getUri() : null;
        if (ringtoneUri == null) {
        if (ringtoneUri != null) {
            return mDefaultVibrationEffect;
        }
        try {
        try {
                effect = mVibrationEffectProxy.get(ringtoneUri, mContext);
            VibrationEffect effect = mVibrationEffectProxy.get(ringtoneUri, mContext);
            if (effect == null) {
            if (effect == null) {
              Log.i(this, "did not find vibration effect, falling back to default vibration");
              Log.i(this, "did not find vibration effect, falling back to default vibration");
              return mDefaultVibrationEffect;
            }
            }
            return effect;
        } catch (IllegalArgumentException iae) {
        } catch (IllegalArgumentException iae) {
            // Deep in the bowels of the VibrationEffect class it is possible for an
            // Deep in the bowels of the VibrationEffect class it is possible for an
            // IllegalArgumentException to be thrown if there is an invalid URI specified in the
            // IllegalArgumentException to be thrown if there is an invalid URI specified in the
            // device config, or a content provider failure.  Rather than crashing the Telecom
            // device config, or a content provider failure.  Rather than crashing the Telecom
            // process we will just use the default vibration effect.
            // process we will just use the default vibration effect.
            Log.e(this, iae, "getVibrationEffectForRingtone: failed to get vibration effect");
            Log.e(this, iae, "getVibrationEffectForRingtone: failed to get vibration effect");
                effect = null;
            return mDefaultVibrationEffect;
            }
        }

        if (effect == null) {
            effect = mDefaultVibrationEffect;
        }
        }
        return effect;
    }
    }


    public void startCallWaiting(Call call) {
    public void startCallWaiting(Call call) {
+6 −1
Original line number Original line Diff line number Diff line
@@ -66,6 +66,9 @@ public class RingtoneFactory {


    public Ringtone getRingtone(Call incomingCall,
    public Ringtone getRingtone(Call incomingCall,
            @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
            @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
        // Initializing ringtones on the main thread can deadlock
        ThreadUtil.checkNotOnMainThread();

        AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(hapticChannelsMuted);
        AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(hapticChannelsMuted);


        // Use the default ringtone of the work profile if the contact is a work profile contact.
        // Use the default ringtone of the work profile if the contact is a work profile contact.
@@ -116,7 +119,7 @@ public class RingtoneFactory {
        return ringtone;
        return ringtone;
    }
    }


    public AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
    private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
        return new AudioAttributes.Builder()
        return new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -127,6 +130,8 @@ public class RingtoneFactory {
    /** Returns a ringtone to be used when ringer is not audible for the incoming call. */
    /** Returns a ringtone to be used when ringer is not audible for the incoming call. */
    @Nullable
    @Nullable
    public Ringtone getHapticOnlyRingtone() {
    public Ringtone getHapticOnlyRingtone() {
        // Initializing ringtones on the main thread can deadlock
        ThreadUtil.checkNotOnMainThread();
        Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
        Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
                com.android.internal.R.string.config_defaultRingtoneVibrationSound));
                com.android.internal.R.string.config_defaultRingtoneVibrationSound));
        AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
        AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
+107 −189

File changed.

Preview size limit exceeded, changes collapsed.