diff options
4 files changed, 544 insertions, 22 deletions
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index 4ff9eb11c142..72f29b431880 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.IBinder; 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.util.proto.ProtoOutputStream; 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 @@ public class VibratorService extends IVibratorService.Stub 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 @@ public class VibratorService extends IVibratorService.Stub 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 @@ public class VibratorService extends IVibratorService.Stub } }; - 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 @@ public class VibratorService extends IVibratorService.Stub } } - // 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 @@ public class VibratorService extends IVibratorService.Stub } 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 @@ public class VibratorService extends IVibratorService.Stub 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 @@ public class VibratorService extends IVibratorService.Stub if (effect == null) { synchronized (mLock) { mAlwaysOnEffects.delete(alwaysOnId); - vibratorAlwaysOnDisable(alwaysOnId); + mNativeWrapper.vibratorAlwaysOnDisable(alwaysOnId); } } else { if (!verifyVibrationEffect(effect)) { @@ -1198,11 +1214,11 @@ public class VibratorService extends IVibratorService.Stub 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 @@ public class VibratorService extends IVibratorService.Stub //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 @@ public class VibratorService extends IVibratorService.Stub // 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 @@ public class VibratorService extends IVibratorService.Stub private void doVibratorSetAmplitude(int amplitude) { if (mSupportsAmplitudeControl) { - vibratorSetAmplitude(amplitude); + mNativeWrapper.vibratorSetAmplitude(amplitude); } } @@ -1291,7 +1307,7 @@ public class VibratorService extends IVibratorService.Stub mInputDeviceVibrators.get(i).cancel(); } } else { - vibratorOff(); + mNativeWrapper.vibratorOff(); } } } finally { @@ -1310,7 +1326,7 @@ public class VibratorService extends IVibratorService.Stub } // 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 @@ public class VibratorService extends IVibratorService.Stub 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 @@ public class VibratorService extends IVibratorService.Stub } } mVibratorUnderExternalControl = externalControl; - vibratorSetExternalControl(externalControl); + mNativeWrapper.vibratorSetExternalControl(externalControl); } private void dumpInternal(PrintWriter pw) { @@ -1688,6 +1704,100 @@ public class VibratorService extends IVibratorService.Stub } } + /** 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 ac2ec58fae2a..7fc6bbd70000 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -59,6 +59,7 @@ android_test { 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 6915220d3fb7..90e1cfcd305a 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 000000000000..e3ad138b5e16 --- /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); + } +} |