Loading services/core/java/com/android/server/vibrator/VibrationThread.java +72 −38 Original line number Diff line number Diff line Loading @@ -45,8 +45,10 @@ import com.android.internal.util.FrameworkStatsLog; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; /** Plays a {@link Vibration} in dedicated thread. */ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Loading Loading @@ -171,7 +173,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Slog.d(TAG, "Synced vibration complete reported by vibrator manager"); } for (int i = 0; i < mVibrators.size(); i++) { mStepQueue.consumeOnVibratorComplete(mVibrators.keyAt(i)); mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i)); } mLock.notify(); } Loading @@ -183,7 +185,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { if (DEBUG) { Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId); } mStepQueue.consumeOnVibratorComplete(vibratorId); mStepQueue.notifyVibratorComplete(vibratorId); mLock.notify(); } } Loading @@ -192,27 +194,26 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration"); try { CombinedVibration.Sequential effect = toSequential(mVibration.getEffect()); int stepsPlayed = 0; synchronized (mLock) { mStepQueue.offer(new StartVibrateStep(effect)); Step topOfQueue; while ((topOfQueue = mStepQueue.peek()) != null) { long waitTime = topOfQueue.calculateWaitTime(); int stepsPlayed = 0; while (!mStepQueue.isEmpty()) { long waitTime = mStepQueue.calculateWaitTime(); if (waitTime <= 0) { stepsPlayed += mStepQueue.consume(); stepsPlayed += mStepQueue.consumeNext(); } else { synchronized (mLock) { try { mLock.wait(waitTime); } catch (InterruptedException e) { } } catch (InterruptedException e) { } } } if (mForceStop) { mStepQueue.cancel(); return Vibration.Status.CANCELLED; } } } // Some effects might be ignored because the specified vibrator don't exist or doesn't // support the effect. We only report ignored here if nothing was played besides the Loading Loading @@ -295,54 +296,75 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { private final class StepQueue { @GuardedBy("mLock") private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>(); @GuardedBy("mLock") private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>(); public void offer(@NonNull Step step) { synchronized (mLock) { mNextSteps.offer(step); } } @GuardedBy("mLock") @Nullable public Step peek() { return mNextSteps.peek(); public boolean isEmpty() { synchronized (mLock) { return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty(); } } /** Returns the time in millis to wait before calling {@link #consumeNext()}. */ public long calculateWaitTime() { Step nextStep; synchronized (mLock) { if (!mPendingOnVibratorCompleteSteps.isEmpty()) { // Steps anticipated by vibrator complete callback should be played right away. return 0; } nextStep = mNextSteps.peek(); } return nextStep == null ? 0 : nextStep.calculateWaitTime(); } /** * Play and remove the step at the top of this queue, and also adds the next steps * generated to be played next. * Play and remove the step at the top of this queue, and also adds the next steps generated * to be played next. * * @return the number of steps played */ @GuardedBy("mLock") public int consume() { Step nextStep = mNextSteps.poll(); public int consumeNext() { Step nextStep = pollNext(); if (nextStep != null) { mNextSteps.addAll(nextStep.play()); // This might turn on the vibrator and have a HAL latency. Execute this outside any // lock to avoid blocking other interactions with the thread. List<Step> nextSteps = nextStep.play(); synchronized (mLock) { mNextSteps.addAll(nextSteps); } return 1; } return 0; } /** * Play and remove the step in this queue that should be anticipated by the vibrator * completion callback. * Notify the step in this queue that should be anticipated by the vibrator completion * callback and keep it separate to be consumed by {@link #consumeNext()}. * * <p>This is a lightweight method that do not trigger any operation from {@link * VibratorController}, so it can be called directly from a native callback. * * <p>This assumes only one of the next steps is waiting on this given vibrator, so the * first step found is played by this method, in no particular order. * first step found will be anticipated by this method, in no particular order. */ @GuardedBy("mLock") public void consumeOnVibratorComplete(int vibratorId) { public void notifyVibratorComplete(int vibratorId) { Iterator<Step> it = mNextSteps.iterator(); List<Step> nextSteps = EMPTY_STEP_LIST; while (it.hasNext()) { Step step = it.next(); if (step.shouldPlayWhenVibratorComplete(vibratorId)) { it.remove(); nextSteps = step.play(); mPendingOnVibratorCompleteSteps.offer(step); break; } } mNextSteps.addAll(nextSteps); } /** Loading @@ -350,13 +372,25 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { * * <p>This will remove and trigger {@link Step#cancel()} in all steps, in order. */ @GuardedBy("mLock") public void cancel() { Step step; while ((step = mNextSteps.poll()) != null) { while ((step = pollNext()) != null) { // This might turn off the vibrator and have a HAL latency. Execute this outside // any lock to avoid blocking other interactions with the thread. step.cancel(); } } @Nullable private Step pollNext() { synchronized (mLock) { // Prioritize the steps anticipated by a vibrator complete callback. if (!mPendingOnVibratorCompleteSteps.isEmpty()) { return mPendingOnVibratorCompleteSteps.poll(); } return mNextSteps.poll(); } } } /** Loading services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +48 −10 Original line number Diff line number Diff line Loading @@ -85,12 +85,17 @@ public class VibrationThreadTest { private static final String PACKAGE_NAME = "package"; private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build(); @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks; @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks; @Mock private IBinder mVibrationToken; @Mock private IBatteryStats mIBatteryStatsMock; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks; @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks; @Mock private IBinder mVibrationToken; @Mock private IBatteryStats mIBatteryStatsMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); private PowerManager.WakeLock mWakeLock; Loading Loading @@ -253,7 +258,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading @@ -278,7 +283,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading Loading @@ -844,6 +849,39 @@ public class VibrationThreadTest { delay < maxDelay); } @LargeTest @Test public void vibrate_cancelSlowVibrator_cancelIsNotBlockedByVibrationThread() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); long latency = 5_000; // 5s fakeVibrator.setLatency(latency); long vibrationId = 1; VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); VibrationThread vibrationThread = startThreadAndDispatcher(vibrationId, effect); assertTrue(waitUntil( t -> !fakeVibrator.getEffectSegments().isEmpty(), vibrationThread, TEST_TIMEOUT_MILLIS)); assertTrue(vibrationThread.isAlive()); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); // Cancelling the vibration should be fast and return right away, even if the thread is // stuck at the slow call to the vibrator. waitForCompletion(cancellingThread, /* timeout= */ 50); // After the vibrator call ends the vibration is cancelled and the vibrator is turned off. waitForCompletion(vibrationThread, /* timeout= */ latency + TEST_TIMEOUT_MILLIS); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); assertFalse(vibrationThread.getVibrators().get(VIBRATOR_ID).isVibrating()); } @Test public void vibrate_multiplePredefinedCancel_cancelsVibrationImmediately() throws Exception { mockVibrators(1, 2); Loading @@ -870,7 +908,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading Loading @@ -902,7 +940,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading Loading
services/core/java/com/android/server/vibrator/VibrationThread.java +72 −38 Original line number Diff line number Diff line Loading @@ -45,8 +45,10 @@ import com.android.internal.util.FrameworkStatsLog; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; /** Plays a {@link Vibration} in dedicated thread. */ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Loading Loading @@ -171,7 +173,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Slog.d(TAG, "Synced vibration complete reported by vibrator manager"); } for (int i = 0; i < mVibrators.size(); i++) { mStepQueue.consumeOnVibratorComplete(mVibrators.keyAt(i)); mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i)); } mLock.notify(); } Loading @@ -183,7 +185,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { if (DEBUG) { Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId); } mStepQueue.consumeOnVibratorComplete(vibratorId); mStepQueue.notifyVibratorComplete(vibratorId); mLock.notify(); } } Loading @@ -192,27 +194,26 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration"); try { CombinedVibration.Sequential effect = toSequential(mVibration.getEffect()); int stepsPlayed = 0; synchronized (mLock) { mStepQueue.offer(new StartVibrateStep(effect)); Step topOfQueue; while ((topOfQueue = mStepQueue.peek()) != null) { long waitTime = topOfQueue.calculateWaitTime(); int stepsPlayed = 0; while (!mStepQueue.isEmpty()) { long waitTime = mStepQueue.calculateWaitTime(); if (waitTime <= 0) { stepsPlayed += mStepQueue.consume(); stepsPlayed += mStepQueue.consumeNext(); } else { synchronized (mLock) { try { mLock.wait(waitTime); } catch (InterruptedException e) { } } catch (InterruptedException e) { } } } if (mForceStop) { mStepQueue.cancel(); return Vibration.Status.CANCELLED; } } } // Some effects might be ignored because the specified vibrator don't exist or doesn't // support the effect. We only report ignored here if nothing was played besides the Loading Loading @@ -295,54 +296,75 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { private final class StepQueue { @GuardedBy("mLock") private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>(); @GuardedBy("mLock") private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>(); public void offer(@NonNull Step step) { synchronized (mLock) { mNextSteps.offer(step); } } @GuardedBy("mLock") @Nullable public Step peek() { return mNextSteps.peek(); public boolean isEmpty() { synchronized (mLock) { return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty(); } } /** Returns the time in millis to wait before calling {@link #consumeNext()}. */ public long calculateWaitTime() { Step nextStep; synchronized (mLock) { if (!mPendingOnVibratorCompleteSteps.isEmpty()) { // Steps anticipated by vibrator complete callback should be played right away. return 0; } nextStep = mNextSteps.peek(); } return nextStep == null ? 0 : nextStep.calculateWaitTime(); } /** * Play and remove the step at the top of this queue, and also adds the next steps * generated to be played next. * Play and remove the step at the top of this queue, and also adds the next steps generated * to be played next. * * @return the number of steps played */ @GuardedBy("mLock") public int consume() { Step nextStep = mNextSteps.poll(); public int consumeNext() { Step nextStep = pollNext(); if (nextStep != null) { mNextSteps.addAll(nextStep.play()); // This might turn on the vibrator and have a HAL latency. Execute this outside any // lock to avoid blocking other interactions with the thread. List<Step> nextSteps = nextStep.play(); synchronized (mLock) { mNextSteps.addAll(nextSteps); } return 1; } return 0; } /** * Play and remove the step in this queue that should be anticipated by the vibrator * completion callback. * Notify the step in this queue that should be anticipated by the vibrator completion * callback and keep it separate to be consumed by {@link #consumeNext()}. * * <p>This is a lightweight method that do not trigger any operation from {@link * VibratorController}, so it can be called directly from a native callback. * * <p>This assumes only one of the next steps is waiting on this given vibrator, so the * first step found is played by this method, in no particular order. * first step found will be anticipated by this method, in no particular order. */ @GuardedBy("mLock") public void consumeOnVibratorComplete(int vibratorId) { public void notifyVibratorComplete(int vibratorId) { Iterator<Step> it = mNextSteps.iterator(); List<Step> nextSteps = EMPTY_STEP_LIST; while (it.hasNext()) { Step step = it.next(); if (step.shouldPlayWhenVibratorComplete(vibratorId)) { it.remove(); nextSteps = step.play(); mPendingOnVibratorCompleteSteps.offer(step); break; } } mNextSteps.addAll(nextSteps); } /** Loading @@ -350,13 +372,25 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { * * <p>This will remove and trigger {@link Step#cancel()} in all steps, in order. */ @GuardedBy("mLock") public void cancel() { Step step; while ((step = mNextSteps.poll()) != null) { while ((step = pollNext()) != null) { // This might turn off the vibrator and have a HAL latency. Execute this outside // any lock to avoid blocking other interactions with the thread. step.cancel(); } } @Nullable private Step pollNext() { synchronized (mLock) { // Prioritize the steps anticipated by a vibrator complete callback. if (!mPendingOnVibratorCompleteSteps.isEmpty()) { return mPendingOnVibratorCompleteSteps.poll(); } return mNextSteps.poll(); } } } /** Loading
services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +48 −10 Original line number Diff line number Diff line Loading @@ -85,12 +85,17 @@ public class VibrationThreadTest { private static final String PACKAGE_NAME = "package"; private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build(); @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks; @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks; @Mock private IBinder mVibrationToken; @Mock private IBatteryStats mIBatteryStatsMock; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks; @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks; @Mock private IBinder mVibrationToken; @Mock private IBatteryStats mIBatteryStatsMock; private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); private PowerManager.WakeLock mWakeLock; Loading Loading @@ -253,7 +258,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading @@ -278,7 +283,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading Loading @@ -844,6 +849,39 @@ public class VibrationThreadTest { delay < maxDelay); } @LargeTest @Test public void vibrate_cancelSlowVibrator_cancelIsNotBlockedByVibrationThread() throws Exception { FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); long latency = 5_000; // 5s fakeVibrator.setLatency(latency); long vibrationId = 1; VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); VibrationThread vibrationThread = startThreadAndDispatcher(vibrationId, effect); assertTrue(waitUntil( t -> !fakeVibrator.getEffectSegments().isEmpty(), vibrationThread, TEST_TIMEOUT_MILLIS)); assertTrue(vibrationThread.isAlive()); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); // Cancelling the vibration should be fast and return right away, even if the thread is // stuck at the slow call to the vibrator. waitForCompletion(cancellingThread, /* timeout= */ 50); // After the vibrator call ends the vibration is cancelled and the vibrator is turned off. waitForCompletion(vibrationThread, /* timeout= */ latency + TEST_TIMEOUT_MILLIS); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); assertFalse(vibrationThread.getVibrators().get(VIBRATOR_ID).isVibrating()); } @Test public void vibrate_multiplePredefinedCancel_cancelsVibrationImmediately() throws Exception { mockVibrators(1, 2); Loading @@ -870,7 +908,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading Loading @@ -902,7 +940,7 @@ public class VibrationThreadTest { Thread cancellingThread = new Thread(() -> vibrationThread.cancel()); cancellingThread.start(); waitForCompletion(vibrationThread, 20); waitForCompletion(vibrationThread, /* timeout= */ 50); waitForCompletion(cancellingThread); verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED)); Loading