Create VibratorServiceTest

Add basic tests to cover main interaction between VibratorService.java
and it's native methods, which will be changed after vibrator HAL
controller is integrated.

Bug: 153418251
Test: atest FrameworksServicesTests:VibratorServiceTest
Change-Id: Ib0122a67d08e380501b71a8652bd4202aae7c002
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 4ff9eb11..72f29b4 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -43,6 +43,7 @@
 import android.os.IExternalVibratorService;
 import android.os.IVibratorService;
 import android.os.IVibratorStateListener;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
@@ -70,6 +71,7 @@
 import android.view.InputDevice;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
@@ -134,7 +136,7 @@
     private final SparseArray<VibrationEffect> mFallbackEffects;
     private final SparseArray<Integer> mProcStatesCache = new SparseArray<>();
     private final WorkSource mTmpWorkSource = new WorkSource();
-    private final Handler mH = new Handler();
+    private final Handler mH;
     private final Object mLock = new Object();
 
     private final Context mContext;
@@ -147,6 +149,7 @@
     private Vibrator mVibrator;
     private SettingsObserver mSettingObserver;
 
+    private final NativeWrapper mNativeWrapper;
     private volatile VibrateThread mThread;
 
     // mInputDeviceVibrators lock should be acquired after mLock, if both are
@@ -208,7 +211,12 @@
         }
     };
 
-    private class Vibration implements IBinder.DeathRecipient {
+    /**
+     * Holder for a vibration to be played. This class can be shared with native methods for
+     * hardware callback support.
+     */
+    @VisibleForTesting
+    public final class Vibration implements IBinder.DeathRecipient {
         public final IBinder token;
         // Start time in CLOCK_BOOTTIME base.
         public final long startTime;
@@ -248,9 +256,9 @@
             }
         }
 
-        // Called by native
-        @SuppressWarnings("unused")
-        private void onComplete() {
+        /** Callback for when vibration is complete, to be called by native. */
+        @VisibleForTesting
+        public void onComplete() {
             synchronized (mLock) {
                 if (this == mCurrentVibration) {
                     doCancelVibrateLocked();
@@ -354,15 +362,23 @@
     }
 
     VibratorService(Context context) {
-        vibratorInit();
+        this(context, new Injector());
+    }
+
+    @VisibleForTesting
+    VibratorService(Context context, Injector injector) {
+        mNativeWrapper = injector.getNativeWrapper();
+        mH = injector.createHandler(Looper.myLooper());
+
+        mNativeWrapper.vibratorInit();
         // Reset the hardware to a default state, in case this is a runtime
         // restart instead of a fresh boot.
-        vibratorOff();
+        mNativeWrapper.vibratorOff();
 
-        mSupportsAmplitudeControl = vibratorSupportsAmplitudeControl();
-        mSupportsExternalControl = vibratorSupportsExternalControl();
-        mSupportedEffects = asList(vibratorGetSupportedEffects());
-        mCapabilities = vibratorGetCapabilities();
+        mSupportsAmplitudeControl = mNativeWrapper.vibratorSupportsAmplitudeControl();
+        mSupportsExternalControl = mNativeWrapper.vibratorSupportsExternalControl();
+        mSupportedEffects = asList(mNativeWrapper.vibratorGetSupportedEffects());
+        mCapabilities = mNativeWrapper.vibratorGetCapabilities();
 
         mContext = context;
         PowerManager pm = context.getSystemService(PowerManager.class);
@@ -419,7 +435,7 @@
         mScaleLevels.put(SCALE_HIGH, new ScaleLevel(SCALE_FACTOR_HIGH));
         mScaleLevels.put(SCALE_VERY_HIGH, new ScaleLevel(SCALE_FACTOR_VERY_HIGH));
 
-        ServiceManager.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
+        injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
     }
 
     private VibrationEffect createEffectFromResource(int resId) {
@@ -642,7 +658,7 @@
         if (effect == null) {
             synchronized (mLock) {
                 mAlwaysOnEffects.delete(alwaysOnId);
-                vibratorAlwaysOnDisable(alwaysOnId);
+                mNativeWrapper.vibratorAlwaysOnDisable(alwaysOnId);
             }
         } else {
             if (!verifyVibrationEffect(effect)) {
@@ -1198,11 +1214,11 @@
     private void updateAlwaysOnLocked(int id, Vibration vib) {
         final int intensity = getCurrentIntensityLocked(vib);
         if (!shouldVibrate(vib, intensity)) {
-            vibratorAlwaysOnDisable(id);
+            mNativeWrapper.vibratorAlwaysOnDisable(id);
         } else {
             final VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.effect;
             final int strength = intensityToEffectStrength(intensity);
-            vibratorAlwaysOnEnable(id, prebaked.getId(), strength);
+            mNativeWrapper.vibratorAlwaysOnEnable(id, prebaked.getId(), strength);
         }
     }
 
@@ -1238,7 +1254,7 @@
         //synchronized (mInputDeviceVibrators) {
         //    return !mInputDeviceVibrators.isEmpty() || vibratorExists();
         //}
-        return vibratorExists();
+        return mNativeWrapper.vibratorExists();
     }
 
     private void doVibratorOn(long millis, int amplitude, int uid, VibrationAttributes attrs) {
@@ -1262,7 +1278,7 @@
                     // Note: ordering is important here! Many haptic drivers will reset their
                     // amplitude when enabled, so we always have to enable first, then set the
                     // amplitude.
-                    vibratorOn(millis);
+                    mNativeWrapper.vibratorOn(millis);
                     doVibratorSetAmplitude(amplitude);
                 }
             }
@@ -1273,7 +1289,7 @@
 
     private void doVibratorSetAmplitude(int amplitude) {
         if (mSupportsAmplitudeControl) {
-            vibratorSetAmplitude(amplitude);
+            mNativeWrapper.vibratorSetAmplitude(amplitude);
         }
     }
 
@@ -1291,7 +1307,7 @@
                         mInputDeviceVibrators.get(i).cancel();
                     }
                 } else {
-                    vibratorOff();
+                    mNativeWrapper.vibratorOff();
                 }
             }
         } finally {
@@ -1310,7 +1326,7 @@
             }
             // Input devices don't support prebaked effect, so skip trying it with them.
             if (!usingInputDeviceVibrators) {
-                long duration = vibratorPerformEffect(prebaked.getId(),
+                long duration = mNativeWrapper.vibratorPerformEffect(prebaked.getId(),
                         prebaked.getEffectStrength(), vib,
                         hasCapability(IVibrator.CAP_PERFORM_CALLBACK));
                 long timeout = duration;
@@ -1363,7 +1379,7 @@
 
             PrimitiveEffect[] primitiveEffects =
                     composed.getPrimitiveEffects().toArray(new PrimitiveEffect[0]);
-            vibratorPerformComposedEffect(primitiveEffects, vib);
+            mNativeWrapper.vibratorPerformComposedEffect(primitiveEffects, vib);
 
             // Composed effects don't actually give us an estimated duration, so we just guess here.
             noteVibratorOnLocked(vib.uid, 10 * primitiveEffects.length);
@@ -1454,7 +1470,7 @@
             }
         }
         mVibratorUnderExternalControl = externalControl;
-        vibratorSetExternalControl(externalControl);
+        mNativeWrapper.vibratorSetExternalControl(externalControl);
     }
 
     private void dumpInternal(PrintWriter pw) {
@@ -1688,6 +1704,100 @@
         }
     }
 
+    /** Wrapper around the static-native methods of {@link VibratorService} for tests. */
+    @VisibleForTesting
+    public static class NativeWrapper {
+
+        /** Checks if vibrator exists on device. */
+        public boolean vibratorExists() {
+            return VibratorService.vibratorExists();
+        }
+
+        /** Initializes connection to vibrator HAL service. */
+        public void vibratorInit() {
+            VibratorService.vibratorInit();
+        }
+
+        /** Turns vibrator on for given time. */
+        public void vibratorOn(long milliseconds) {
+            VibratorService.vibratorOn(milliseconds);
+        }
+
+        /** Turns vibrator off. */
+        public void vibratorOff() {
+            VibratorService.vibratorOff();
+        }
+
+        /** Returns true if vibrator supports {@link #vibratorSetAmplitude(int)}. */
+        public boolean vibratorSupportsAmplitudeControl() {
+            return VibratorService.vibratorSupportsAmplitudeControl();
+        }
+
+        /** Sets the amplitude for the vibrator to run. */
+        public void vibratorSetAmplitude(int amplitude) {
+            VibratorService.vibratorSetAmplitude(amplitude);
+        }
+
+        /** Returns all predefined effects supported by the device vibrator. */
+        public int[] vibratorGetSupportedEffects() {
+            return VibratorService.vibratorGetSupportedEffects();
+        }
+
+        /** Turns vibrator on to perform one of the supported effects. */
+        public long vibratorPerformEffect(long effect, long strength, Vibration vibration,
+                boolean withCallback) {
+            return VibratorService.vibratorPerformEffect(effect, strength, vibration, withCallback);
+        }
+
+        /** Turns vibrator on to perform one of the supported composed effects. */
+        public void vibratorPerformComposedEffect(
+                VibrationEffect.Composition.PrimitiveEffect[] effect, Vibration vibration) {
+            VibratorService.vibratorPerformComposedEffect(effect, vibration);
+        }
+
+        /** Returns true if vibrator supports {@link #vibratorSetExternalControl(boolean)}. */
+        public boolean vibratorSupportsExternalControl() {
+            return VibratorService.vibratorSupportsExternalControl();
+        }
+
+        /** Enabled the device vibrator to be controlled by another service. */
+        public void vibratorSetExternalControl(boolean enabled) {
+            VibratorService.vibratorSetExternalControl(enabled);
+        }
+
+        /** Returns all capabilities of the device vibrator. */
+        public long vibratorGetCapabilities() {
+            return VibratorService.vibratorGetCapabilities();
+        }
+
+        /** Enable always-on vibration with given id and effect. */
+        public void vibratorAlwaysOnEnable(long id, long effect, long strength) {
+            VibratorService.vibratorAlwaysOnEnable(id, effect, strength);
+        }
+
+        /** Disable always-on vibration for given id. */
+        public void vibratorAlwaysOnDisable(long id) {
+            VibratorService.vibratorAlwaysOnDisable(id);
+        }
+    }
+
+    /** Point of injection for test dependencies */
+    @VisibleForTesting
+    static class Injector {
+
+        NativeWrapper getNativeWrapper() {
+            return new NativeWrapper();
+        }
+
+        Handler createHandler(Looper looper) {
+            return new Handler(looper);
+        }
+
+        void addService(String name, IBinder service) {
+            ServiceManager.addService(name, service);
+        }
+    }
+
     BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ac2ec58f..7fc6bbd7 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -59,6 +59,7 @@
     libs: [
         "android.hardware.power-java",
         "android.hardware.tv.cec-V1.0-java",
+        "android.hardware.vibrator-java",
         "android.hidl.manager-V1.0-java",
         "android.test.mock",
         "android.test.base",
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 6915220..90e1cfc 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -81,6 +81,9 @@
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
     <uses-permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE"/>
     <uses-permission android:name="android.permission.MEDIA_RESOURCE_OVERRIDE_PID"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE"/>
+    <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON"/>
 
     <!-- Uses API introduced in O (26) -->
     <uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
new file mode 100644
index 0000000..e3ad138
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
+import android.hardware.vibrator.IVibrator;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IVibratorStateListener;
+import android.os.Looper;
+import android.os.PowerManagerInternal;
+import android.os.Process;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Tests for {@link VibratorService}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:VibratorServiceTest
+ */
+@Presubmit
+public class VibratorServiceTest {
+
+    private static final int UID = Process.ROOT_UID;
+    private static final String PACKAGE_NAME = "package";
+    private static final VibrationAttributes ALARM_ATTRS =
+            new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build();
+
+    @Rule public MockitoRule rule = MockitoJUnit.rule();
+
+    @Mock private PackageManagerInternal mPackageManagerInternalMock;
+    @Mock private PowerManagerInternal mPowerManagerInternalMock;
+    @Mock private VibratorService.NativeWrapper mNativeWrapperMock;
+    @Mock private IVibratorStateListener mVibratorStateListenerMock;
+    @Mock private IBinder mVibratorStateListenerBinderMock;
+
+    private TestLooper mTestLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestLooper = new TestLooper();
+
+        when(mVibratorStateListenerMock.asBinder()).thenReturn(mVibratorStateListenerBinderMock);
+        when(mPackageManagerInternalMock.getSystemUiServiceComponent())
+                .thenReturn(new ComponentName("", ""));
+
+        addLocalServiceMock(PackageManagerInternal.class, mPackageManagerInternalMock);
+        addLocalServiceMock(PowerManagerInternal.class, mPowerManagerInternalMock);
+    }
+
+    private VibratorService createService() {
+        return new VibratorService(InstrumentationRegistry.getContext(),
+                new VibratorService.Injector() {
+                    @Override
+                    VibratorService.NativeWrapper getNativeWrapper() {
+                        return mNativeWrapperMock;
+                    }
+
+                    @Override
+                    Handler createHandler(Looper looper) {
+                        return new Handler(mTestLooper.getLooper());
+                    }
+
+                    @Override
+                    void addService(String name, IBinder service) {
+                        // ignore
+                    }
+                });
+    }
+
+    @Test
+    public void createService_initializesNativeService() {
+        createService();
+        verify(mNativeWrapperMock).vibratorInit();
+        verify(mNativeWrapperMock).vibratorOff();
+    }
+
+    @Test
+    public void hasVibrator_withVibratorHalPresent_returnsTrue() {
+        when(mNativeWrapperMock.vibratorExists()).thenReturn(true);
+        assertTrue(createService().hasVibrator());
+    }
+
+    @Test
+    public void hasVibrator_withNoVibratorHalPresent_returnsFalse() {
+        when(mNativeWrapperMock.vibratorExists()).thenReturn(false);
+        assertFalse(createService().hasVibrator());
+    }
+
+    @Test
+    public void hasAmplitudeControl_withAmplitudeControlSupport_returnsTrue() {
+        when(mNativeWrapperMock.vibratorSupportsAmplitudeControl()).thenReturn(true);
+        assertTrue(createService().hasAmplitudeControl());
+    }
+
+    @Test
+    public void hasAmplitudeControl_withNoAmplitudeControlSupport_returnsFalse() {
+        when(mNativeWrapperMock.vibratorSupportsAmplitudeControl()).thenReturn(false);
+        assertFalse(createService().hasAmplitudeControl());
+    }
+
+    @Test
+    public void areEffectsSupported_withNullResultFromNative_returnsSupportUnknown() {
+        when(mNativeWrapperMock.vibratorGetSupportedEffects()).thenReturn(null);
+        assertArrayEquals(new int[]{Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN},
+                createService().areEffectsSupported(new int[]{VibrationEffect.EFFECT_CLICK}));
+    }
+
+    @Test
+    public void areEffectsSupported_withSomeEffectsSupported_returnsSupportYesAndNoForEffects() {
+        int[] effects = new int[]{VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK};
+
+        when(mNativeWrapperMock.vibratorGetSupportedEffects())
+                .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
+        assertArrayEquals(
+                new int[]{Vibrator.VIBRATION_EFFECT_SUPPORT_YES,
+                        Vibrator.VIBRATION_EFFECT_SUPPORT_NO},
+                createService().areEffectsSupported(effects));
+    }
+
+    @Test
+    public void arePrimitivesSupported_withoutComposeCapability_returnsAlwaysFalse() {
+        assertArrayEquals(new boolean[]{false, false},
+                createService().arePrimitivesSupported(new int[]{
+                        VibrationEffect.Composition.PRIMITIVE_CLICK,
+                        VibrationEffect.Composition.PRIMITIVE_TICK
+                }));
+    }
+
+    @Test
+    public void arePrimitivesSupported_withComposeCapability_returnsAlwaysTrue() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        assertArrayEquals(new boolean[]{true, true},
+                createService().arePrimitivesSupported(new int[]{
+                        VibrationEffect.Composition.PRIMITIVE_CLICK,
+                        VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
+                }));
+    }
+
+    @Test
+    public void setAlwaysOnEffect_withCapabilityAndValidEffect_enablesAlwaysOnEffect() {
+        mockVibratorCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+
+        assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1,
+                VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS));
+        verify(mNativeWrapperMock).vibratorAlwaysOnEnable(
+                eq(1L), eq((long) VibrationEffect.EFFECT_CLICK),
+                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG));
+    }
+
+    @Test
+    public void setAlwaysOnEffect_withNonPrebakedEffect_ignoresEffect() {
+        mockVibratorCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+
+        assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1,
+                VibrationEffect.createOneShot(100, 255), ALARM_ATTRS));
+        verify(mNativeWrapperMock, never()).vibratorAlwaysOnDisable(anyLong());
+        verify(mNativeWrapperMock, never()).vibratorAlwaysOnEnable(anyLong(), anyLong(), anyLong());
+    }
+
+    @Test
+    public void setAlwaysOnEffect_withNullEffect_disablesAlwaysOnEffect() {
+        mockVibratorCapabilities(IVibrator.CAP_ALWAYS_ON_CONTROL);
+
+        assertTrue(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1, null, ALARM_ATTRS));
+        verify(mNativeWrapperMock).vibratorAlwaysOnDisable(eq(1L));
+    }
+
+    @Test
+    public void setAlwaysOnEffect_withoutCapability_ignoresEffect() {
+        assertFalse(createService().setAlwaysOnEffect(UID, PACKAGE_NAME, 1,
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK), ALARM_ATTRS));
+        verify(mNativeWrapperMock, never()).vibratorAlwaysOnDisable(anyLong());
+        verify(mNativeWrapperMock, never()).vibratorAlwaysOnEnable(anyLong(), anyLong(), anyLong());
+    }
+
+    @Test
+    public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude() {
+        when(mNativeWrapperMock.vibratorSupportsAmplitudeControl()).thenReturn(true);
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        vibrate(service, VibrationEffect.createOneShot(100, 128));
+        assertTrue(service.isVibrating());
+
+        verify(mNativeWrapperMock).vibratorOff();
+        verify(mNativeWrapperMock).vibratorOn(eq(100L));
+        verify(mNativeWrapperMock).vibratorSetAmplitude(eq(128));
+    }
+
+    @Test
+    public void vibrate_withOneShotAndNoAmplitudeControl_turnsVibratorOnAndIgnoresAmplitude() {
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        vibrate(service, VibrationEffect.createOneShot(100, 128));
+        assertTrue(service.isVibrating());
+
+        verify(mNativeWrapperMock).vibratorOff();
+        verify(mNativeWrapperMock).vibratorOn(eq(100L));
+        verify(mNativeWrapperMock, never()).vibratorSetAmplitude(anyInt());
+    }
+
+    @Test
+    public void vibrate_withPrebaked_performsEffect() {
+        when(mNativeWrapperMock.vibratorGetSupportedEffects())
+                .thenReturn(new int[]{VibrationEffect.EFFECT_CLICK});
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        vibrate(service, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
+
+        verify(mNativeWrapperMock).vibratorOff();
+        verify(mNativeWrapperMock).vibratorPerformEffect(
+                eq((long) VibrationEffect.EFFECT_CLICK),
+                eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG),
+                any(VibratorService.Vibration.class), eq(false));
+    }
+
+    @Test
+    public void vibrate_withComposed_performsEffect() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
+                .compose();
+        vibrate(service, effect);
+
+        ArgumentCaptor<VibrationEffect.Composition.PrimitiveEffect[]> primitivesCaptor =
+                ArgumentCaptor.forClass(VibrationEffect.Composition.PrimitiveEffect[].class);
+
+        verify(mNativeWrapperMock).vibratorOff();
+        verify(mNativeWrapperMock).vibratorPerformComposedEffect(
+                primitivesCaptor.capture(), any(VibratorService.Vibration.class));
+
+        // Check all primitive effect fields are passed down to the HAL.
+        assertEquals(1, primitivesCaptor.getValue().length);
+        VibrationEffect.Composition.PrimitiveEffect primitive = primitivesCaptor.getValue()[0];
+        assertEquals(VibrationEffect.Composition.PRIMITIVE_CLICK, primitive.id);
+        assertEquals(0.5f, primitive.scale, /* delta= */ 1e-2);
+        assertEquals(10, primitive.delay);
+    }
+
+    @Test
+    public void vibrate_withCallback_finishesVibrationWhenCallbackTriggered() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        doAnswer(invocation -> {
+            ((VibratorService.Vibration) invocation.getArgument(1)).onComplete();
+            return null;
+        }).when(mNativeWrapperMock).vibratorPerformComposedEffect(
+                any(), any(VibratorService.Vibration.class));
+
+        // Use vibration with delay so there is time for the callback to be triggered.
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 10)
+                .compose();
+        vibrate(service, effect);
+
+        // Vibration canceled once before perform and once by native callback.
+        verify(mNativeWrapperMock, times(2)).vibratorOff();
+        verify(mNativeWrapperMock).vibratorPerformComposedEffect(
+                any(VibrationEffect.Composition.PrimitiveEffect[].class),
+                any(VibratorService.Vibration.class));
+    }
+
+    @Test
+    public void vibrate_whenBinderDies_cancelsVibration() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        doAnswer(invocation -> {
+            ((VibratorService.Vibration) invocation.getArgument(1)).binderDied();
+            return null;
+        }).when(mNativeWrapperMock).vibratorPerformComposedEffect(
+                any(), any(VibratorService.Vibration.class));
+
+        // Use vibration with delay so there is time for the callback to be triggered.
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 10)
+                .compose();
+        vibrate(service, effect);
+
+        // Vibration canceled once before perform and once by native binder death.
+        verify(mNativeWrapperMock, times(2)).vibratorOff();
+        verify(mNativeWrapperMock).vibratorPerformComposedEffect(
+                any(VibrationEffect.Composition.PrimitiveEffect[].class),
+                any(VibratorService.Vibration.class));
+    }
+
+    @Test
+    public void cancelVibrate_withDeviceVibrating_callsVibratorOff() {
+        VibratorService service = createService();
+        vibrate(service, VibrationEffect.createOneShot(100, 128));
+        assertTrue(service.isVibrating());
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        service.cancelVibrate(service);
+        assertFalse(service.isVibrating());
+        verify(mNativeWrapperMock).vibratorOff();
+    }
+
+    @Test
+    public void cancelVibrate_withDeviceNotVibrating_ignoresCall() {
+        VibratorService service = createService();
+        Mockito.clearInvocations(mNativeWrapperMock);
+
+        service.cancelVibrate(service);
+        assertFalse(service.isVibrating());
+        verify(mNativeWrapperMock, never()).vibratorOff();
+    }
+
+    @Test
+    public void registerVibratorStateListener_callbacksAreTriggered() throws Exception {
+        VibratorService service = createService();
+
+        service.registerVibratorStateListener(mVibratorStateListenerMock);
+        verify(mVibratorStateListenerMock).onVibrating(false);
+
+        vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
+        verify(mVibratorStateListenerMock).onVibrating(true);
+
+        // Run the scheduled callback to finish one-shot vibration.
+        mTestLooper.moveTimeForward(10);
+        mTestLooper.dispatchAll();
+        verify(mVibratorStateListenerMock, times(2)).onVibrating(false);
+    }
+
+    @Test
+    public void unregisterVibratorStateListener_callbackNotTriggeredAfter() throws Exception {
+        VibratorService service = createService();
+
+        service.registerVibratorStateListener(mVibratorStateListenerMock);
+        verify(mVibratorStateListenerMock).onVibrating(false);
+
+        vibrate(service, VibrationEffect.createOneShot(5, VibrationEffect.DEFAULT_AMPLITUDE));
+        verify(mVibratorStateListenerMock).onVibrating(true);
+
+        service.unregisterVibratorStateListener(mVibratorStateListenerMock);
+        Mockito.clearInvocations(mVibratorStateListenerMock);
+
+        vibrate(service, VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE));
+        verifyNoMoreInteractions(mVibratorStateListenerMock);
+    }
+
+    private void vibrate(VibratorService service, VibrationEffect effect) {
+        service.vibrate(UID, PACKAGE_NAME, effect, ALARM_ATTRS, "some reason", service);
+    }
+
+    private void mockVibratorCapabilities(int capabilities) {
+        when(mNativeWrapperMock.vibratorGetCapabilities()).thenReturn((long) capabilities);
+    }
+
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+}