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

Commit 08091da7 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement VendorVibrationSession.vibrate" into main

parents 55fc3d50 1cbf97ad
Loading
Loading
Loading
Loading
+91 −13
Original line number Original line Diff line number Diff line
@@ -37,8 +37,11 @@ import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoOutputStream;


import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.NoSuchElementException;


@@ -60,6 +63,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub
         * used for another vibration.
         * used for another vibration.
         */
         */
        void onSessionReleased(long sessionId);
        void onSessionReleased(long sessionId);

        /** Request the manager to trigger a vibration within this session. */
        void vibrate(long sessionId, CallerInfo callerInfo, CombinedVibration vibration);
    }
    }


    private final Object mLock = new Object();
    private final Object mLock = new Object();
@@ -71,7 +77,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub
    private final IVibrationSessionCallback mCallback;
    private final IVibrationSessionCallback mCallback;
    private final CallerInfo mCallerInfo;
    private final CallerInfo mCallerInfo;
    private final VibratorManagerHooks mManagerHooks;
    private final VibratorManagerHooks mManagerHooks;
    private final DeviceAdapter mDeviceAdapter;
    private final Handler mHandler;
    private final Handler mHandler;
    private final List<DebugInfo> mVibrations = new ArrayList<>();


    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private Status mStatus = Status.RUNNING;
    private Status mStatus = Status.RUNNING;
@@ -83,24 +91,28 @@ final class VendorVibrationSession extends IVibrationSession.Stub
    private long mEndUptime;
    private long mEndUptime;
    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private long mEndTime; // for debugging
    private long mEndTime; // for debugging
    @GuardedBy("mLock")
    private VibrationStepConductor mConductor;


    VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler,
    VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler,
            @NonNull VibratorManagerHooks managerHooks, @NonNull int[] vibratorIds,
            @NonNull VibratorManagerHooks managerHooks, @NonNull DeviceAdapter deviceAdapter,
            @NonNull IVibrationSessionCallback callback) {
            @NonNull IVibrationSessionCallback callback) {
        mCreateUptime = SystemClock.uptimeMillis();
        mCreateUptime = SystemClock.uptimeMillis();
        mCreateTime = System.currentTimeMillis();
        mCreateTime = System.currentTimeMillis();
        mVibratorIds = vibratorIds;
        mVibratorIds = deviceAdapter.getAvailableVibratorIds();
        mHandler = handler;
        mHandler = handler;
        mCallback = callback;
        mCallback = callback;
        mCallerInfo = callerInfo;
        mCallerInfo = callerInfo;
        mManagerHooks = managerHooks;
        mManagerHooks = managerHooks;
        mDeviceAdapter = deviceAdapter;
        CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this);
        CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this);
    }
    }


    @Override
    @Override
    public void vibrate(CombinedVibration vibration, String reason) {
    public void vibrate(CombinedVibration vibration, String reason) {
        // TODO(b/345414356): implement vibration support
        CallerInfo vibrationCallerInfo = new CallerInfo(mCallerInfo.attrs, mCallerInfo.uid,
        throw new UnsupportedOperationException("Vendor session vibrations not yet implemented");
                mCallerInfo.deviceId, mCallerInfo.opPkg, reason);
        mManagerHooks.vibrate(mSessionId, vibrationCallerInfo, vibration);
    }
    }


    @Override
    @Override
@@ -146,7 +158,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
    public DebugInfo getDebugInfo() {
    public DebugInfo getDebugInfo() {
        synchronized (mLock) {
        synchronized (mLock) {
            return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime,
            return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime,
                    mEndUptime, mEndTime);
                    mEndUptime, mEndTime, mVibrations);
        }
        }
    }
    }


@@ -200,12 +212,12 @@ final class VendorVibrationSession extends IVibrationSession.Stub


    @Override
    @Override
    public void notifyVibratorCallback(int vibratorId, long vibrationId) {
    public void notifyVibratorCallback(int vibratorId, long vibrationId) {
        // TODO(b/345414356): implement vibration support
        // Ignore it, the session vibration playback doesn't depend on HAL timings
    }
    }


    @Override
    @Override
    public void notifySyncedVibratorsCallback(long vibrationId) {
    public void notifySyncedVibratorsCallback(long vibrationId) {
        // TODO(b/345414356): implement vibration support
        // Ignore it, the session vibration playback doesn't depend on HAL timings
    }
    }


    @Override
    @Override
@@ -214,8 +226,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub
            // If end was not requested then the HAL has cancelled the session.
            // If end was not requested then the HAL has cancelled the session.
            maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON);
            maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON);
            maybeSetStatusToRequestedLocked();
            maybeSetStatusToRequestedLocked();
            clearVibrationConductor();
        }
        }
        mManagerHooks.onSessionReleased(mSessionId);
        mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId));
    }
    }


    @Override
    @Override
@@ -228,7 +241,8 @@ final class VendorVibrationSession extends IVibrationSession.Stub
                    /* includeDate= */ true))
                    /* includeDate= */ true))
                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
                    + ", callerInfo: " + mCallerInfo
                    + ", callerInfo: " + mCallerInfo
                    + ", vibratorIds: " + Arrays.toString(mVibratorIds);
                    + ", vibratorIds: " + Arrays.toString(mVibratorIds)
                    + ", vibrations: " + mVibrations;
        }
        }
    }
    }


@@ -254,6 +268,13 @@ final class VendorVibrationSession extends IVibrationSession.Stub
        return mVibratorIds;
        return mVibratorIds;
    }
    }


    @VisibleForTesting
    public List<DebugInfo> getVibrations() {
        synchronized (mLock) {
            return new ArrayList<>(mVibrations);
        }
    }

    public ICancellationSignal getCancellationSignal() {
    public ICancellationSignal getCancellationSignal() {
        return mCancellationSignal;
        return mCancellationSignal;
    }
    }
@@ -278,7 +299,39 @@ final class VendorVibrationSession extends IVibrationSession.Stub
        }
        }
        if (isAlreadyEnded) {
        if (isAlreadyEnded) {
            // Session already ended, make sure we end it in the HAL.
            // Session already ended, make sure we end it in the HAL.
            mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true);
            mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true));
        }
    }

    public void notifyVibrationAttempt(DebugInfo vibrationDebugInfo) {
        mVibrations.add(vibrationDebugInfo);
    }

    @Nullable
    public VibrationStepConductor clearVibrationConductor() {
        synchronized (mLock) {
            VibrationStepConductor conductor = mConductor;
            if (conductor != null) {
                mVibrations.add(conductor.getVibration().getDebugInfo());
            }
            mConductor = null;
            return conductor;
        }
    }

    public DeviceAdapter getDeviceAdapter() {
        return mDeviceAdapter;
    }

    public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) {
        synchronized (mLock) {
            if (mConductor != null) {
                Slog.d(TAG, "Vibration session still dispatching previous vibration,"
                        + " new vibration ignored");
                return false;
            }
            mConductor = conductor;
            return true;
        }
        }
    }
    }


@@ -296,7 +349,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
            }
            }
        }
        }
        if (shouldTriggerSessionHook) {
        if (shouldTriggerSessionHook) {
            mManagerHooks.endSession(mSessionId, shouldAbort);
            mHandler.post(() ->  mManagerHooks.endSession(mSessionId, shouldAbort));
        }
        }
    }
    }


@@ -309,6 +362,11 @@ final class VendorVibrationSession extends IVibrationSession.Stub
        mEndStatusRequest = status;
        mEndStatusRequest = status;
        mEndTime = System.currentTimeMillis();
        mEndTime = System.currentTimeMillis();
        mEndUptime = SystemClock.uptimeMillis();
        mEndUptime = SystemClock.uptimeMillis();
        if (mConductor != null) {
            // Vibration is being dispatched when session end was requested, cancel it.
            mConductor.notifyCancelled(new Vibration.EndInfo(status),
                    /* immediate= */ status != Status.FINISHED);
        }
        if (isStarted()) {
        if (isStarted()) {
            // Only trigger "finishing" callback if session started.
            // Only trigger "finishing" callback if session started.
            // Run client callback in separate thread.
            // Run client callback in separate thread.
@@ -377,6 +435,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
    static final class DebugInfoImpl implements VibrationSession.DebugInfo {
    static final class DebugInfoImpl implements VibrationSession.DebugInfo {
        private final Status mStatus;
        private final Status mStatus;
        private final CallerInfo mCallerInfo;
        private final CallerInfo mCallerInfo;
        private final List<DebugInfo> mVibrations;


        private final long mCreateUptime;
        private final long mCreateUptime;
        private final long mCreateTime;
        private final long mCreateTime;
@@ -385,7 +444,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
        private final long mDurationMs;
        private final long mDurationMs;


        DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime,
        DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime,
                long startTime, long endUptime, long endTime) {
                long startTime, long endUptime, long endTime, List<DebugInfo> vibrations) {
            mStatus = status;
            mStatus = status;
            mCallerInfo = callerInfo;
            mCallerInfo = callerInfo;
            mCreateUptime = createUptime;
            mCreateUptime = createUptime;
@@ -393,6 +452,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
            mStartTime = startTime;
            mStartTime = startTime;
            mEndTime = endTime;
            mEndTime = endTime;
            mDurationMs = endUptime > 0 ? endUptime - createUptime : -1;
            mDurationMs = endUptime > 0 ? endUptime - createUptime : -1;
            mVibrations = vibrations == null ? new ArrayList<>() : new ArrayList<>(vibrations);
        }
        }


        @Override
        @Override
@@ -418,6 +478,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub


        @Override
        @Override
        public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
        public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
            for (DebugInfo vibration : mVibrations) {
                vibration.logMetrics(statsLogger);
            }
        }
        }


        @Override
        @Override
@@ -448,6 +511,14 @@ final class VendorVibrationSession extends IVibrationSession.Stub
            pw.println("endTime = " + (mEndTime == 0 ? null
            pw.println("endTime = " + (mEndTime == 0 ? null
                    : formatTime(mEndTime, /*includeDate=*/ true)));
                    : formatTime(mEndTime, /*includeDate=*/ true)));
            pw.println("callerInfo = " + mCallerInfo);
            pw.println("callerInfo = " + mCallerInfo);

            pw.println("vibrations:");
            pw.increaseIndent();
            for (DebugInfo vibration : mVibrations) {
                vibration.dump(pw);
            }
            pw.decreaseIndent();

            pw.decreaseIndent();
            pw.decreaseIndent();
        }
        }


@@ -477,6 +548,12 @@ final class VendorVibrationSession extends IVibrationSession.Stub
                    " | %s (uid=%d, deviceId=%d) | reason: %s",
                    " | %s (uid=%d, deviceId=%d) | reason: %s",
                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason);
                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason);
            pw.println(timingsStr + paramStr + audioUsageStr + callerStr);
            pw.println(timingsStr + paramStr + audioUsageStr + callerStr);

            pw.increaseIndent();
            for (DebugInfo vibration : mVibrations) {
                vibration.dumpCompact(pw);
            }
            pw.decreaseIndent();
        }
        }


        @Override
        @Override
@@ -487,7 +564,8 @@ final class VendorVibrationSession extends IVibrationSession.Stub
                    /* includeDate= */ true))
                    /* includeDate= */ true))
                    + ", durationMs: " + mDurationMs
                    + ", durationMs: " + mDurationMs
                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
                    + ", callerInfo: " + mCallerInfo;
                    + ", callerInfo: " + mCallerInfo
                    + ", vibrations: " + mVibrations;
        }
        }
    }
    }
}
}
+201 −47

File changed.

Preview size limit exceeded, changes collapsed.

+220 −1
Original line number Original line Diff line number Diff line
@@ -2865,7 +2865,7 @@ public class VibratorManagerServiceTest {
        mTestLooper.dispatchAll();
        mTestLooper.dispatchAll();


        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 3}));
        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
        verify(callback, never()).onStarted(any(IVibrationSession.class));
        verify(callback, never()).onStarted(any(IVibrationSession.class));
        verify(callback, never()).onFinishing();
        verify(callback, never()).onFinishing();
        verify(callback)
        verify(callback)
@@ -2889,6 +2889,7 @@ public class VibratorManagerServiceTest {
        verify(callback).onStarted(captor.capture());
        verify(callback).onStarted(captor.capture());


        captor.getValue().finishSession();
        captor.getValue().finishSession();
        mTestLooper.dispatchAll();


        // Session not ended until HAL callback.
        // Session not ended until HAL callback.
        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
@@ -3138,6 +3139,224 @@ public class VibratorManagerServiceTest {
        verify(callback).onStarted(any(IVibrationSession.class));
        verify(callback).onStarted(any(IVibrationSession.class));
    }
    }


    @Test
    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void vibrateInSession_afterCancel_vibrationIgnored() throws Exception {
        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mockVibrators(1, 2);
        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
        fakeVibrator1.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        VibratorManagerService service = createSystemReadyService();
        int sessionFinishDelayMs = 200;
        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);

        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
        mTestLooper.dispatchAll();

        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
        verify(callback).onStarted(captor.capture());

        IVibrationSession startedSession = captor.getValue();
        startedSession.cancelSession();
        startedSession.vibrate(
                CombinedVibration.createParallel(VibrationEffect.createOneShot(10, 255)),
                "reason");

        // VibrationThread will never start this vibration.
        assertFalse(waitUntil(s -> !fakeVibrator1.getAmplitudes().isEmpty(), service,
                TEST_TIMEOUT_MILLIS));

        // Dispatch HAL callbacks.
        mTestLooper.moveTimeForward(sessionFinishDelayMs);
        mTestLooper.dispatchAll();

        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER);
        verify(callback).onFinishing();
        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
    }

    @Test
    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void vibrateInSession_afterFinish_vibrationIgnored() throws Exception {
        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mockVibrators(1, 2);
        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
        fakeVibrator1.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        VibratorManagerService service = createSystemReadyService();
        int sessionFinishDelayMs = 200;
        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);

        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
        mTestLooper.dispatchAll();

        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
        verify(callback).onStarted(captor.capture());

        IVibrationSession startedSession = captor.getValue();
        startedSession.finishSession();
        mTestLooper.dispatchAll();

        startedSession.vibrate(
                CombinedVibration.createParallel(VibrationEffect.createOneShot(10, 255)),
                "reason");

        // Session not ended until HAL callback.
        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);

        // VibrationThread will never start this vibration.
        assertFalse(waitUntil(s -> !fakeVibrator1.getAmplitudes().isEmpty(), service,
                TEST_TIMEOUT_MILLIS));

        // Dispatch HAL callbacks.
        mTestLooper.moveTimeForward(sessionFinishDelayMs);
        mTestLooper.dispatchAll();

        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
        verify(callback).onFinishing();
        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
    }

    @Test
    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void vibrateInSession_repeatingVibration_vibrationIgnored() throws Exception {
        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mockVibrators(1, 2);
        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
        fakeVibrator1.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        VibratorManagerService service = createSystemReadyService();
        int sessionFinishDelayMs = 200;
        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);

        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
        mTestLooper.dispatchAll();

        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
        verify(callback).onStarted(captor.capture());

        IVibrationSession startedSession = captor.getValue();
        startedSession.vibrate(
                CombinedVibration.createParallel(
                        VibrationEffect.createWaveform(new long[]{ 10, 10, 10, 10}, 0)),
                "reason");

        // VibrationThread will never start this vibration.
        assertFalse(waitUntil(s -> !fakeVibrator1.getAmplitudes().isEmpty(), service,
                TEST_TIMEOUT_MILLIS));

        startedSession.finishSession();
        mTestLooper.dispatchAll();

        // Dispatch HAL callbacks.
        mTestLooper.moveTimeForward(sessionFinishDelayMs);
        mTestLooper.dispatchAll();

        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
        assertThat(service.isVibrating(1)).isFalse();
        assertThat(service.isVibrating(2)).isFalse();
        verify(callback).onFinishing();
        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
    }

    @Test
    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void vibrateInSession_singleVibration_playsAllVibrateCommands() throws Exception {
        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mockVibrators(1, 2);
        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
        fakeVibrator1.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        FakeVibratorControllerProvider fakeVibrator2 = mVibratorProviders.get(1);
        fakeVibrator2.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        VibratorManagerService service = createSystemReadyService();
        int sessionFinishDelayMs = 200;
        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);

        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
        mTestLooper.dispatchAll();

        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
        verify(callback).onStarted(captor.capture());

        IVibrationSession startedSession = captor.getValue();
        startedSession.vibrate(
                CombinedVibration.createParallel(
                        VibrationEffect.createWaveform(new long[]{ 10, 10, 10, 10}, -1)),
                "reason");

        // VibrationThread will start this vibration async, so wait until vibration is triggered.
        // Vibrators will receive 2 requests for the waveform playback
        assertTrue(waitUntil(s -> fakeVibrator1.getAmplitudes().size() == 2, service,
                TEST_TIMEOUT_MILLIS));
        assertTrue(waitUntil(s -> fakeVibrator2.getAmplitudes().size() == 2, service,
                TEST_TIMEOUT_MILLIS));

        startedSession.finishSession();
        mTestLooper.dispatchAll();

        // Dispatch HAL callbacks.
        mTestLooper.moveTimeForward(sessionFinishDelayMs);
        mTestLooper.dispatchAll();

        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
        assertThat(service.isVibrating(1)).isFalse();
        assertThat(service.isVibrating(2)).isFalse();
        verify(callback).onFinishing();
        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
    }

    @Test
    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
    public void vibrateInSession_multipleVibrations_playsAllVibrations() throws Exception {
        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
        mockVibrators(1, 2);
        FakeVibratorControllerProvider fakeVibrator1 = mVibratorProviders.get(1);
        fakeVibrator1.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
        VibratorManagerService service = createSystemReadyService();
        int sessionFinishDelayMs = 200;
        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);

        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
        mTestLooper.dispatchAll();

        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
        verify(callback).onStarted(captor.capture());

        IVibrationSession startedSession = captor.getValue();
        startedSession.vibrate(
                CombinedVibration.createParallel(VibrationEffect.createOneShot(10, 255)),
                "reason");

        // VibrationThread will start this vibration async, so wait until vibration is completed.
        assertTrue(waitUntil(s -> fakeVibrator1.getAmplitudes().size() == 1, service,
                TEST_TIMEOUT_MILLIS));
        assertTrue(waitUntil(s -> !session.getVibrations().isEmpty(), service,
                TEST_TIMEOUT_MILLIS));

        startedSession.vibrate(
                CombinedVibration.createParallel(VibrationEffect.createOneShot(20, 255)),
                "reason");

        assertTrue(waitUntil(s -> fakeVibrator1.getAmplitudes().size() == 2, service,
                TEST_TIMEOUT_MILLIS));

        startedSession.finishSession();
        mTestLooper.dispatchAll();

        // Dispatch HAL callbacks.
        mTestLooper.moveTimeForward(sessionFinishDelayMs);
        mTestLooper.dispatchAll();

        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
        assertThat(service.isVibrating(1)).isFalse();
        assertThat(service.isVibrating(2)).isFalse();
        verify(callback).onFinishing();
        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
    }

    @Test
    @Test
    public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception {
    public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception {
        mockVibrators(1);
        mockVibrators(1);