Loading src/com/android/server/telecom/AsyncRingtonePlayer.java +8 −2 Original line number Diff line number Diff line Loading @@ -140,9 +140,15 @@ public class AsyncRingtonePlayer { return; } Ringtone ringtone = null; boolean hasStopped = false; try { ringtone = ringtoneSupplier.get(); // Ringtone supply can be slow. Re-check for stop event. if (mHandler.hasMessages(EVENT_STOP)) { hasStopped = true; ringtone.stop(); // proactively release the ringtone. return; } // setRingtone even if null - it also stops any current ringtone to be consistent // with the overall state. setRingtone(ringtone); Loading @@ -162,7 +168,7 @@ public class AsyncRingtonePlayer { mRingtone.play(); Log.i(this, "Play ringtone, looping."); } finally { ringtoneConsumer.accept(ringtone, /* stopped= */ false); ringtoneConsumer.accept(ringtone, hasStopped); } } finally { Log.cancelSubsession(session); Loading src/com/android/server/telecom/Ringer.java +51 −21 Original line number Diff line number Diff line Loading @@ -176,7 +176,7 @@ public class Ringer { /** * Call objects that are ringing, vibrating or call-waiting. These are used only for logging * purposes. * purposes (except mVibratingCall is also used to ensure consistency). */ private Call mRingingCall; private Call mVibratingCall; Loading Loading @@ -406,17 +406,32 @@ public class Ringer { foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted); } // If vibration will be done, reserve the vibrator. boolean vibratorReserved = isVibratorEnabled && attributes.shouldRingForContact() && tryReserveVibration(foregroundCall); if (!vibratorReserved) { foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE); Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, " + "isVibratorEnabled=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), isVibratorEnabled); } // 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) -> { BiConsumer<Ringtone, Boolean> afterRingtoneLogic = (Ringtone ringtone, Boolean stopped) -> { try { if (stopped.booleanValue()) { return; // don't start vibration if the ringing is already abandoned. if (stopped.booleanValue() || !vibratorReserved) { // don't start vibration if the ringing is already abandoned, or the // vibrator wasn't reserved. This still triggers the mBlockOnRingingFuture. return; } final VibrationEffect vibrationEffect; if (ringtone != null && finalUseCustomVibrationEffect) { Loading @@ -431,8 +446,7 @@ public class Ringer { boolean isUsingAudioCoupledHaptics = !finalHapticChannelsMuted && ringtone != null && ringtone.hasHapticChannels(); vibrateIfNeeded(isUsingAudioCoupledHaptics, finalAttributes, foregroundCall, vibrationEffect, isVibratorEnabled); vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect); } finally { // This is used to signal to tests that the async play() call has completed. if (mBlockOnRingingFuture != null) { Loading @@ -442,9 +456,9 @@ public class Ringer { }; deferBlockOnRingingFuture = true; // Run in vibrationLogic. if (ringtoneSupplier != null) { mRingtonePlayer.play(ringtoneSupplier, vibrationLogic); mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic); } else { vibrationLogic.accept(/* ringtone= */ null, /* stopped= */ false); afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false); } // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here Loading @@ -460,10 +474,31 @@ public class Ringer { } } private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, RingerAttributes attributes, Call foregroundCall, VibrationEffect effect, boolean isVibratorEnabled) { final boolean shouldRingForContact = attributes.shouldRingForContact(); /** * Try to reserve the vibrator for this call, returning false if it's already committed. * The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the * audio. The logic uses mVibratingCall to say which call is currently getting ready to vibrate, * or actually vibrating (indicated by mIsVibrating). * * Once reserved, the vibrateIfNeeded method is expected to be called. Note that if * audio-coupled haptics were used instead of vibrator, the reservation still stays until * ringing is stopped, because the vibrator is exclusive to a single vibration source. * * Note that this "reservation" is only local to the Ringer - it's not locking the vibrator, so * if it's busy with some other important vibration, this ringer's one may not displace it. */ private boolean tryReserveVibration(Call foregroundCall) { synchronized (mLock) { if (mVibratingCall != null || mIsVibrating) { return false; } mVibratingCall = foregroundCall; return true; } } private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, Call foregroundCall, VibrationEffect effect) { if (isUsingAudioCoupledHaptics) { Log.addEvent( foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics"); Loading @@ -471,22 +506,17 @@ public class Ringer { } synchronized (mLock) { if (isVibratorEnabled && !mIsVibrating && shouldRingForContact) { // Ensure the reservation is live. The mIsVibrating check should be redundant. if (foregroundCall == mVibratingCall && !mIsVibrating) { Log.addEvent(foregroundCall, LogUtils.Events.START_VIBRATOR, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), mIsVibrating); mVibratingCall = foregroundCall; mIsVibrating = true; mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES); Log.i(this, "start vibration."); } else { foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE); Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), mIsVibrating); } // else stopped already: this isn't started unless a reservation was made. } } Loading Loading @@ -565,8 +595,8 @@ public class Ringer { Log.addEvent(mVibratingCall, LogUtils.Events.STOP_VIBRATOR); mVibrator.cancel(); mIsVibrating = false; mVibratingCall = null; } mVibratingCall = null; // Prevents vibrations from starting via AsyncRingtonePlayer. } } Loading tests/src/com/android/server/telecom/tests/RingerTest.java +15 −1 Original line number Diff line number Diff line Loading @@ -505,8 +505,20 @@ public class RingerTest extends TelecomTestCase { @Test public void testStopFlashNotificationWhenRingStops() throws Exception { ensureRingtoneMocked(); Ringtone mockRingtone = mock(Ringtone.class); when(mockRingtoneFactory.getRingtone( any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean())) .thenAnswer(x -> { // Be slow to create ringtone. try { Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return mockRingtone; }); // Start call waiting to make sure that it doesn't stop when we start ringing enableVibrationWhenRinging(); mRingerUnderTest.startCallWaiting(mockCall1); when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall2.getHandle()).thenReturn(Uri.parse("")); Loading @@ -518,6 +530,8 @@ public class RingerTest extends TelecomTestCase { verify(mockAccessibilityManagerAdapter, atLeastOnce()) .stopFlashNotificationSequence(any(Context.class)); mRingCompletionFuture.get(); // Don't leak async work. verify(mockVibrator, never()) // cancelled before it started. .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class)); } @SmallTest Loading Loading
src/com/android/server/telecom/AsyncRingtonePlayer.java +8 −2 Original line number Diff line number Diff line Loading @@ -140,9 +140,15 @@ public class AsyncRingtonePlayer { return; } Ringtone ringtone = null; boolean hasStopped = false; try { ringtone = ringtoneSupplier.get(); // Ringtone supply can be slow. Re-check for stop event. if (mHandler.hasMessages(EVENT_STOP)) { hasStopped = true; ringtone.stop(); // proactively release the ringtone. return; } // setRingtone even if null - it also stops any current ringtone to be consistent // with the overall state. setRingtone(ringtone); Loading @@ -162,7 +168,7 @@ public class AsyncRingtonePlayer { mRingtone.play(); Log.i(this, "Play ringtone, looping."); } finally { ringtoneConsumer.accept(ringtone, /* stopped= */ false); ringtoneConsumer.accept(ringtone, hasStopped); } } finally { Log.cancelSubsession(session); Loading
src/com/android/server/telecom/Ringer.java +51 −21 Original line number Diff line number Diff line Loading @@ -176,7 +176,7 @@ public class Ringer { /** * Call objects that are ringing, vibrating or call-waiting. These are used only for logging * purposes. * purposes (except mVibratingCall is also used to ensure consistency). */ private Call mRingingCall; private Call mVibratingCall; Loading Loading @@ -406,17 +406,32 @@ public class Ringer { foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted); } // If vibration will be done, reserve the vibrator. boolean vibratorReserved = isVibratorEnabled && attributes.shouldRingForContact() && tryReserveVibration(foregroundCall); if (!vibratorReserved) { foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE); Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, " + "isVibratorEnabled=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), isVibratorEnabled); } // 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) -> { BiConsumer<Ringtone, Boolean> afterRingtoneLogic = (Ringtone ringtone, Boolean stopped) -> { try { if (stopped.booleanValue()) { return; // don't start vibration if the ringing is already abandoned. if (stopped.booleanValue() || !vibratorReserved) { // don't start vibration if the ringing is already abandoned, or the // vibrator wasn't reserved. This still triggers the mBlockOnRingingFuture. return; } final VibrationEffect vibrationEffect; if (ringtone != null && finalUseCustomVibrationEffect) { Loading @@ -431,8 +446,7 @@ public class Ringer { boolean isUsingAudioCoupledHaptics = !finalHapticChannelsMuted && ringtone != null && ringtone.hasHapticChannels(); vibrateIfNeeded(isUsingAudioCoupledHaptics, finalAttributes, foregroundCall, vibrationEffect, isVibratorEnabled); vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect); } finally { // This is used to signal to tests that the async play() call has completed. if (mBlockOnRingingFuture != null) { Loading @@ -442,9 +456,9 @@ public class Ringer { }; deferBlockOnRingingFuture = true; // Run in vibrationLogic. if (ringtoneSupplier != null) { mRingtonePlayer.play(ringtoneSupplier, vibrationLogic); mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic); } else { vibrationLogic.accept(/* ringtone= */ null, /* stopped= */ false); afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false); } // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here Loading @@ -460,10 +474,31 @@ public class Ringer { } } private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, RingerAttributes attributes, Call foregroundCall, VibrationEffect effect, boolean isVibratorEnabled) { final boolean shouldRingForContact = attributes.shouldRingForContact(); /** * Try to reserve the vibrator for this call, returning false if it's already committed. * The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the * audio. The logic uses mVibratingCall to say which call is currently getting ready to vibrate, * or actually vibrating (indicated by mIsVibrating). * * Once reserved, the vibrateIfNeeded method is expected to be called. Note that if * audio-coupled haptics were used instead of vibrator, the reservation still stays until * ringing is stopped, because the vibrator is exclusive to a single vibration source. * * Note that this "reservation" is only local to the Ringer - it's not locking the vibrator, so * if it's busy with some other important vibration, this ringer's one may not displace it. */ private boolean tryReserveVibration(Call foregroundCall) { synchronized (mLock) { if (mVibratingCall != null || mIsVibrating) { return false; } mVibratingCall = foregroundCall; return true; } } private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, Call foregroundCall, VibrationEffect effect) { if (isUsingAudioCoupledHaptics) { Log.addEvent( foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics"); Loading @@ -471,22 +506,17 @@ public class Ringer { } synchronized (mLock) { if (isVibratorEnabled && !mIsVibrating && shouldRingForContact) { // Ensure the reservation is live. The mIsVibrating check should be redundant. if (foregroundCall == mVibratingCall && !mIsVibrating) { Log.addEvent(foregroundCall, LogUtils.Events.START_VIBRATOR, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), mIsVibrating); mVibratingCall = foregroundCall; mIsVibrating = true; mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES); Log.i(this, "start vibration."); } else { foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE); Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION, "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, isVibrating=%b", mVibrator.hasVibrator(), mSystemSettingsUtil.isRingVibrationEnabled(mContext), mAudioManager.getRingerMode(), mIsVibrating); } // else stopped already: this isn't started unless a reservation was made. } } Loading Loading @@ -565,8 +595,8 @@ public class Ringer { Log.addEvent(mVibratingCall, LogUtils.Events.STOP_VIBRATOR); mVibrator.cancel(); mIsVibrating = false; mVibratingCall = null; } mVibratingCall = null; // Prevents vibrations from starting via AsyncRingtonePlayer. } } Loading
tests/src/com/android/server/telecom/tests/RingerTest.java +15 −1 Original line number Diff line number Diff line Loading @@ -505,8 +505,20 @@ public class RingerTest extends TelecomTestCase { @Test public void testStopFlashNotificationWhenRingStops() throws Exception { ensureRingtoneMocked(); Ringtone mockRingtone = mock(Ringtone.class); when(mockRingtoneFactory.getRingtone( any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean())) .thenAnswer(x -> { // Be slow to create ringtone. try { Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return mockRingtone; }); // Start call waiting to make sure that it doesn't stop when we start ringing enableVibrationWhenRinging(); mRingerUnderTest.startCallWaiting(mockCall1); when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false); when(mockCall2.getHandle()).thenReturn(Uri.parse("")); Loading @@ -518,6 +530,8 @@ public class RingerTest extends TelecomTestCase { verify(mockAccessibilityManagerAdapter, atLeastOnce()) .stopFlashNotificationSequence(any(Context.class)); mRingCompletionFuture.get(); // Don't leak async work. verify(mockVibrator, never()) // cancelled before it started. .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class)); } @SmallTest Loading