From e320282bba390267a88fad37752a5243c9730052 Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Thu, 22 Oct 2020 13:15:09 +0000 Subject: [PATCH] Extract input device vibrators code from VibratorService This code will also be migrated to VibratorManagerService, to give preference to input devices over the devices vibrators when the user setting enables it. This need to remain a server-side code so permissions/setting will be applied to system vibrations even when they are redirected to input devices (including vibration scaling). Bug: 167946816 Bug: 131311651 Test: atest FrameworksServiceTests:VibratorServiceTest atest FrameworksServiceTests:InputDeviceVibratorsTest Change-Id: I6c61c3f20e4d82d44af136917d3755d9f6edd31f --- .../android/hardware/input/InputManager.java | 18 +- core/java/android/view/InputDevice.java | 10 +- .../com/android/server/VibratorService.java | 193 +++++------- .../server/vibrator/InputDeviceDelegate.java | 171 ++++++++++ .../android/server/VibratorServiceTest.java | 96 ++++++ .../vibrator/InputDeviceDelegateTest.java | 291 ++++++++++++++++++ 6 files changed, 653 insertions(+), 126 deletions(-) create mode 100644 services/core/java/com/android/server/vibrator/InputDeviceDelegate.java create mode 100644 services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 7fb73ceab4f4a..81ea2f5a17dde 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -56,6 +56,7 @@ import android.view.PointerIcon; import android.view.VerifiedInputEvent; import android.view.WindowManager.LayoutParams; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; @@ -268,6 +269,21 @@ public final class InputManager { mIm = im; } + /** + * Gets an instance of the input manager. + * + * @return The input manager instance. + * + * @hide + */ + @VisibleForTesting + public static InputManager resetInstance(IInputManager inputManagerService) { + synchronized (InputManager.class) { + sInstance = new InputManager(inputManagerService); + return sInstance; + } + } + /** * Gets an instance of the input manager. * @@ -1428,7 +1444,7 @@ public final class InputManager { @Override public boolean hasAmplitudeControl() { - return false; + return true; } /** diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 22ac4dcd2cfe3..8da833a28efa9 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -29,6 +29,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Vibrator; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -423,9 +425,13 @@ public final class InputDevice implements Parcelable { } }; - // Called by native code. + /** + * Called by native code + * @hide + */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private InputDevice(int id, int generation, int controllerNumber, String name, int vendorId, + @VisibleForTesting + public InputDevice(int id, int generation, int controllerNumber, String name, int vendorId, int productId, String descriptor, boolean isExternal, int sources, int keyboardType, KeyCharacterMap keyCharacterMap, boolean hasVibrator, boolean hasMicrophone, boolean hasButtonUnderPad) { diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index e662553af7310..94d3a6bf70b8d 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -29,7 +29,6 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Resources; -import android.hardware.input.InputManager; import android.hardware.vibrator.IVibrator; import android.os.BatteryStats; import android.os.Binder; @@ -60,13 +59,13 @@ import android.os.WorkSource; import android.util.Slog; import android.util.SparseArray; 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; +import com.android.server.vibrator.InputDeviceDelegate; import com.android.server.vibrator.VibrationScaler; import com.android.server.vibrator.VibrationSettings; @@ -82,15 +81,15 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -public class VibratorService extends IVibratorService.Stub - implements InputManager.InputDeviceListener { +/** System implementation of {@link IVibratorService}. */ +public class VibratorService extends IVibratorService.Stub { private static final String TAG = "VibratorService"; private static final SimpleDateFormat DEBUG_DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); private static final boolean DEBUG = false; private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service"; - private static final long[] DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS = { 0, 30, 100, 30 }; + private static final long[] DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS = {0, 30, 100, 30}; // Default vibration attributes. Used when vibration is requested without attributes private static final VibrationAttributes DEFAULT_ATTRIBUTES = @@ -122,19 +121,13 @@ public class VibratorService extends IVibratorService.Stub private final IBatteryStats mBatteryStatsService; private final String mSystemUiPackage; private PowerManagerInternal mPowerManagerInternal; - private InputManager mIm; private VibrationSettings mVibrationSettings; private VibrationScaler mVibrationScaler; + private InputDeviceDelegate mInputDeviceDelegate; private final NativeWrapper mNativeWrapper; private volatile VibrateWaveformThread mThread; - // mInputDeviceVibrators lock should be acquired after mLock, if both are - // to be acquired - private final ArrayList mInputDeviceVibrators = new ArrayList<>(); - private boolean mVibrateInputDevicesSetting; // guarded by mInputDeviceVibrators - private boolean mInputDeviceListenerRegistered; // guarded by mInputDeviceVibrators - @GuardedBy("mLock") private Vibration mCurrentVibration; private int mCurVibUid = -1; @@ -343,6 +336,7 @@ public class VibratorService extends IVibratorService.Stub public enum Status { RUNNING, FINISHED, + FORWARDED_TO_INPUT_DEVICES, CANCELLED, ERROR_APP_OPS, IGNORED, @@ -577,9 +571,9 @@ public class VibratorService extends IVibratorService.Stub public void systemReady() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorService#systemReady"); try { - mIm = mContext.getSystemService(InputManager.class); mVibrationSettings = new VibrationSettings(mContext, mH); mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings); + mInputDeviceDelegate = new InputDeviceDelegate(mContext, mH); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); mPowerManagerInternal.registerLowPowerModeObserver( @@ -707,12 +701,8 @@ public class VibratorService extends IVibratorService.Stub @Override // Binder call public boolean hasAmplitudeControl() { - synchronized (mInputDeviceVibrators) { - // Input device vibrators don't support amplitude controls yet, but are still used over - // the system vibrator when connected. - return hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL) - && mInputDeviceVibrators.isEmpty(); - } + // Input device vibrators always support amplitude controls. + return mInputDeviceDelegate.isAvailable() || hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL); } @Override // Binder call @@ -1059,10 +1049,8 @@ public class VibratorService extends IVibratorService.Stub Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); doVibratorOn(vib); } else if (vib.effect instanceof VibrationEffect.Waveform) { - // mThread better be null here. doCancelVibrate should always be - // called before startNextVibrationLocked or startVibrationLocked. - mThread = new VibrateWaveformThread(vib); - mThread.start(); + Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + doVibratorWaveformEffectLocked(vib); } else if (vib.effect instanceof VibrationEffect.Prebaked) { Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); doVibratorPrebakedEffectLocked(vib); @@ -1193,7 +1181,8 @@ public class VibratorService extends IVibratorService.Stub private void updateVibrators() { synchronized (mLock) { - boolean devicesUpdated = updateInputDeviceVibratorsLocked(); + boolean devicesUpdated = mInputDeviceDelegate.updateInputDeviceVibrators( + mVibrationSettings.shouldVibrateInputDevices()); boolean lowPowerModeUpdated = updateLowPowerModeLocked(); if (devicesUpdated || lowPowerModeUpdated) { @@ -1205,41 +1194,6 @@ public class VibratorService extends IVibratorService.Stub } } - private boolean updateInputDeviceVibratorsLocked() { - boolean changed = false; - boolean vibrateInputDevices = mVibrationSettings.shouldVibrateInputDevices(); - if (vibrateInputDevices != mVibrateInputDevicesSetting) { - changed = true; - mVibrateInputDevicesSetting = vibrateInputDevices; - } - - if (mVibrateInputDevicesSetting) { - if (!mInputDeviceListenerRegistered) { - mInputDeviceListenerRegistered = true; - mIm.registerInputDeviceListener(this, mH); - } - } else { - if (mInputDeviceListenerRegistered) { - mInputDeviceListenerRegistered = false; - mIm.unregisterInputDeviceListener(this); - } - } - - mInputDeviceVibrators.clear(); - if (mVibrateInputDevicesSetting) { - int[] ids = mIm.getInputDeviceIds(); - for (int i = 0; i < ids.length; i++) { - InputDevice device = mIm.getInputDevice(ids[i]); - Vibrator vibrator = device.getVibrator(); - if (vibrator.hasVibrator()) { - mInputDeviceVibrators.add(vibrator); - } - } - return true; - } - return changed; - } - private boolean updateLowPowerModeLocked() { boolean lowPowerMode = mPowerManagerInternal .getLowPowerState(ServiceType.VIBRATION).batterySaverEnabled; @@ -1268,57 +1222,36 @@ public class VibratorService extends IVibratorService.Stub } } - @Override - public void onInputDeviceAdded(int deviceId) { - updateVibrators(); - } - - @Override - public void onInputDeviceChanged(int deviceId) { - updateVibrators(); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - updateVibrators(); - } - private boolean doVibratorExists() { // For now, we choose to ignore the presence of input devices that have vibrators // when reporting whether the device has a vibrator. Applications often use this // information to decide whether to enable certain features so they expect the // result of hasVibrator() to be constant. For now, just report whether // the device has a built-in vibrator. - //synchronized (mInputDeviceVibrators) { - // return !mInputDeviceVibrators.isEmpty() || vibratorExists(); - //} return mNativeWrapper.vibratorExists(); } private void doVibratorOn(Vibration vib) { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOn"); try { - synchronized (mInputDeviceVibrators) { - final VibrationEffect.OneShot oneShot = vib.effect.resolve( - mDefaultVibrationAmplitude); - if (DEBUG) { - Slog.d(TAG, "Turning vibrator on for " + oneShot.getDuration() + " ms" - + " with amplitude " + oneShot.getAmplitude() + "."); - } + final VibrationEffect.OneShot oneShot = vib.effect.resolve(mDefaultVibrationAmplitude); + if (DEBUG) { + Slog.d(TAG, "Turning vibrator on for " + oneShot.getDuration() + " ms" + + " with amplitude " + oneShot.getAmplitude() + "."); + } + boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable( + vib.uid, vib.opPkg, oneShot, vib.reason, vib.attrs); + if (inputDevicesAvailable) { + // The set current vibration is no longer being played by this service, so drop it. + mCurrentVibration = null; + endVibrationLocked(vib, VibrationInfo.Status.FORWARDED_TO_INPUT_DEVICES); + } else { noteVibratorOnLocked(vib.uid, oneShot.getDuration()); - final int vibratorCount = mInputDeviceVibrators.size(); - if (vibratorCount != 0) { - for (int i = 0; i < vibratorCount; i++) { - mInputDeviceVibrators.get(i).vibrate(vib.uid, vib.opPkg, oneShot, - vib.reason, vib.attrs); - } - } else { - // 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. - mNativeWrapper.vibratorOn(oneShot.getDuration(), vib.id); - doVibratorSetAmplitude(oneShot.getAmplitude()); - } + // 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. + mNativeWrapper.vibratorOn(oneShot.getDuration(), vib.id); + doVibratorSetAmplitude(oneShot.getAmplitude()); } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); @@ -1334,19 +1267,34 @@ public class VibratorService extends IVibratorService.Stub private void doVibratorOff() { Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorOff"); try { - synchronized (mInputDeviceVibrators) { - if (DEBUG) { - Slog.d(TAG, "Turning vibrator off."); - } - noteVibratorOffLocked(); - final int vibratorCount = mInputDeviceVibrators.size(); - if (vibratorCount != 0) { - for (int i = 0; i < vibratorCount; i++) { - mInputDeviceVibrators.get(i).cancel(); - } - } else { - mNativeWrapper.vibratorOff(); - } + if (DEBUG) { + Slog.d(TAG, "Turning vibrator off."); + } + noteVibratorOffLocked(); + boolean inputDevicesAvailable = mInputDeviceDelegate.cancelVibrateIfAvailable(); + if (!inputDevicesAvailable) { + mNativeWrapper.vibratorOff(); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + + @GuardedBy("mLock") + private void doVibratorWaveformEffectLocked(Vibration vib) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorWaveformEffectLocked"); + try { + boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable( + vib.uid, vib.opPkg, vib.effect, vib.reason, vib.attrs); + if (inputDevicesAvailable) { + // The set current vibration is no longer being played by this service, so drop it. + mCurrentVibration = null; + endVibrationLocked(vib, VibrationInfo.Status.FORWARDED_TO_INPUT_DEVICES); + } else { + // mThread better be null here. doCancelVibrate should always be + // called before startNextVibrationLocked or startVibrationLocked. + mThread = new VibrateWaveformThread(vib); + mThread.start(); } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); @@ -1358,12 +1306,9 @@ public class VibratorService extends IVibratorService.Stub Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "doVibratorPrebakedEffectLocked"); try { final VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.effect; - final boolean usingInputDeviceVibrators; - synchronized (mInputDeviceVibrators) { - usingInputDeviceVibrators = !mInputDeviceVibrators.isEmpty(); - } - // Input devices don't support prebaked effect, so skip trying it with them. - if (!usingInputDeviceVibrators) { + // Input devices don't support prebaked effect, so skip trying it with them and allow + // fallback to be attempted. + if (!mInputDeviceDelegate.isAvailable()) { long duration = mNativeWrapper.vibratorPerformEffect( prebaked.getId(), prebaked.getEffectStrength(), vib.id); if (duration > 0) { @@ -1401,15 +1346,17 @@ public class VibratorService extends IVibratorService.Stub try { final VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.effect; - final boolean usingInputDeviceVibrators; - synchronized (mInputDeviceVibrators) { - usingInputDeviceVibrators = !mInputDeviceVibrators.isEmpty(); - } - // Input devices don't support composed effect, so skip trying it with them. - if (usingInputDeviceVibrators || !hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { - endVibrationLocked(vib, VibrationInfo.Status.IGNORED_UNSUPPORTED); + boolean inputDevicesAvailable = mInputDeviceDelegate.vibrateIfAvailable( + vib.uid, vib.opPkg, composed, vib.reason, vib.attrs); + if (inputDevicesAvailable) { + // The set current vibration is no longer being played by this service, so drop it. + mCurrentVibration = null; + endVibrationLocked(vib, VibrationInfo.Status.FORWARDED_TO_INPUT_DEVICES); + return; + } else if (!hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) { // The set current vibration is not actually playing, so drop it. mCurrentVibration = null; + endVibrationLocked(vib, VibrationInfo.Status.IGNORED_UNSUPPORTED); return; } diff --git a/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java new file mode 100644 index 0000000000000..edbc05802697a --- /dev/null +++ b/services/core/java/com/android/server/vibrator/InputDeviceDelegate.java @@ -0,0 +1,171 @@ +/* + * 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.vibrator; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.SparseArray; +import android.view.InputDevice; + +import com.android.internal.annotations.GuardedBy; + +/** Delegates vibrations to all connected {@link InputDevice} with available {@link Vibrator}. */ +// TODO(b/159207608): Make this package-private once vibrator services are moved to this package +public final class InputDeviceDelegate implements InputManager.InputDeviceListener { + private static final String TAG = "InputDeviceDelegate"; + + private final Object mLock = new Object(); + private final Handler mHandler; + private final InputManager mInputManager; + + @GuardedBy("mLock") + private final SparseArray mInputDeviceVibrators = new SparseArray<>(); + + /** + * Flag updated via {@link #updateInputDeviceVibrators(boolean)}, holding the value of {@link + * android.provider.Settings.System#VIBRATE_INPUT_DEVICES}. + */ + @GuardedBy("mLock") + private boolean mShouldVibrateInputDevices; + + public InputDeviceDelegate(Context context, Handler handler) { + mHandler = handler; + mInputManager = context.getSystemService(InputManager.class); + } + + @Override + public void onInputDeviceAdded(int deviceId) { + updateInputDevice(deviceId); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + updateInputDevice(deviceId); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + synchronized (mLock) { + mInputDeviceVibrators.remove(deviceId); + } + } + + /** + * Return {@code true} is there are input devices with vibrators available and vibrations should + * be delegated to them. + */ + public boolean isAvailable() { + synchronized (mLock) { + // mInputDeviceVibrators is cleared when settings are disabled, so this check is enough. + return mInputDeviceVibrators.size() > 0; + } + } + + /** + * Vibrate all {@link InputDevice} with {@link Vibrator} available using given effect. + * + * @return {@link #isAvailable()} + */ + public boolean vibrateIfAvailable(int uid, String opPkg, VibrationEffect effect, + String reason, VibrationAttributes attrs) { + synchronized (mLock) { + for (int i = 0; i < mInputDeviceVibrators.size(); i++) { + mInputDeviceVibrators.valueAt(i).vibrate(uid, opPkg, effect, reason, attrs); + } + return mInputDeviceVibrators.size() > 0; + } + } + + /** + * Cancel vibration on all {@link InputDevice} with {@link Vibrator} available. + * + * @return {@link #isAvailable()} + */ + public boolean cancelVibrateIfAvailable() { + synchronized (mLock) { + for (int i = 0; i < mInputDeviceVibrators.size(); i++) { + mInputDeviceVibrators.valueAt(i).cancel(); + } + return mInputDeviceVibrators.size() > 0; + } + } + + /** + * Updates the list of {@link InputDevice} vibrators based on the {@link + * VibrationSettings#shouldVibrateInputDevices()} setting current value and the + * devices currently available in {@link InputManager#getInputDeviceIds()}. + * + * @return true if there was any change in input devices available or related settings. + */ + public boolean updateInputDeviceVibrators(boolean vibrateInputDevices) { + synchronized (mLock) { + if (vibrateInputDevices == mShouldVibrateInputDevices) { + // No need to update if settings haven't changed. + return false; + } + + mShouldVibrateInputDevices = vibrateInputDevices; + mInputDeviceVibrators.clear(); + + if (vibrateInputDevices) { + // Register the listener first so any device added/updated/removed after the call to + // getInputDeviceIds() will trigger the callbacks (which will wait on the lock for + // this loop to finish). + mInputManager.registerInputDeviceListener(this, mHandler); + + for (int deviceId : mInputManager.getInputDeviceIds()) { + InputDevice device = mInputManager.getInputDevice(deviceId); + if (device == null) { + continue; + } + Vibrator vibrator = device.getVibrator(); + if (vibrator.hasVibrator()) { + mInputDeviceVibrators.put(device.getId(), vibrator); + } + } + } else { + mInputManager.unregisterInputDeviceListener(this); + } + } + + return true; + } + + private void updateInputDevice(int deviceId) { + synchronized (mLock) { + if (!mShouldVibrateInputDevices) { + // No need to keep this device vibrator if setting is off. + return; + } + InputDevice device = mInputManager.getInputDevice(deviceId); + if (device == null) { + mInputDeviceVibrators.remove(deviceId); + return; + } + Vibrator vibrator = device.getVibrator(); + if (vibrator.hasVibrator()) { + mInputDeviceVibrators.put(deviceId, vibrator); + } else { + mInputDeviceVibrators.remove(deviceId); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java index 6152421db6590..34302caf0bffc 100644 --- a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java @@ -43,6 +43,8 @@ import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.PackageManagerInternal; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; import android.hardware.vibrator.IVibrator; import android.media.AudioAttributes; import android.media.AudioManager; @@ -63,6 +65,7 @@ import android.os.Vibrator; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.provider.Settings; +import android.view.InputDevice; import androidx.test.InstrumentationRegistry; @@ -120,6 +123,7 @@ public class VibratorServiceTest { @Mock private AppOpsManager mAppOpsManagerMock; @Mock private VibratorService.NativeWrapper mNativeWrapperMock; @Mock private IVibratorStateListener mVibratorStateListenerMock; + @Mock private IInputManager mIInputManagerMock; @Mock private IBinder mVibratorStateListenerBinderMock; private TestLooper mTestLooper; @@ -129,10 +133,12 @@ public class VibratorServiceTest { public void setUp() throws Exception { mTestLooper = new TestLooper(); mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); + InputManager inputManager = InputManager.resetInstance(mIInputManagerMock); ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy); when(mContextSpy.getContentResolver()).thenReturn(contentResolver); when(mContextSpy.getSystemService(eq(Context.VIBRATOR_SERVICE))).thenReturn(mVibratorMock); + when(mContextSpy.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager); when(mContextSpy.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManagerMock); when(mVibratorMock.getDefaultHapticFeedbackIntensity()) .thenReturn(Vibrator.VIBRATION_INTENSITY_MEDIUM); @@ -145,6 +151,7 @@ public class VibratorServiceTest { .thenReturn(new ComponentName("", "")); when(mPowerManagerInternalMock.getLowPowerState(PowerManager.ServiceType.VIBRATION)) .thenReturn(mPowerSaveStateMock); + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1); setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, @@ -216,6 +223,15 @@ public class VibratorServiceTest { assertFalse(createService().hasAmplitudeControl()); } + @Test + public void hasAmplitudeControl_withInputDevices_returnsTrue() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + assertTrue(createService().hasAmplitudeControl()); + } + @Test public void areEffectsSupported_withNullResultFromNative_returnsSupportUnknown() { when(mNativeWrapperMock.vibratorGetSupportedEffects()).thenReturn(null); @@ -374,6 +390,22 @@ public class VibratorServiceTest { eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString()); } + @Test + public void vibrate_withOneShotAndInputDevices_vibratesInputDevices() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + VibratorService service = createService(); + Mockito.clearInvocations(mNativeWrapperMock); + + VibrationEffect effect = VibrationEffect.createOneShot(100, 128); + vibrate(service, effect); + assertFalse(service.isVibrating()); + + verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any()); + verify(mNativeWrapperMock, never()).vibratorOn(anyLong(), anyLong()); + } + @Test public void vibrate_withOneShotAndAmplitudeControl_turnsVibratorOnAndSetsAmplitude() { mockVibratorCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -415,6 +447,25 @@ public class VibratorServiceTest { eq((long) VibrationEffect.EFFECT_STRENGTH_STRONG), gt(0L)); } + @Test + public void vibrate_withPrebakedAndInputDevices_vibratesFallbackWaveformOnInputDevices() + throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + VibratorService service = createService(); + Mockito.clearInvocations(mNativeWrapperMock); + + vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + assertFalse(service.isVibrating()); + + // Wait for VibrateThread to turn input device vibrator ON. + Thread.sleep(5); + verify(mIInputManagerMock).vibrate(eq(1), any(), any()); + verify(mNativeWrapperMock, never()).vibratorOn(anyLong(), anyLong()); + verify(mNativeWrapperMock, never()).vibratorPerformEffect(anyLong(), anyLong(), anyLong()); + } + @Test public void vibrate_withComposed_performsEffect() { mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); @@ -441,6 +492,27 @@ public class VibratorServiceTest { assertEquals(10, primitive.delay); } + @Test + public void vibrate_withComposedAndInputDevices_vibratesInputDevices() + throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + when(mIInputManagerMock.getInputDevice(2)).thenReturn(createInputDeviceWithVibrator(2)); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + VibratorService service = createService(); + Mockito.clearInvocations(mNativeWrapperMock); + + VibrationEffect effect = VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .compose(); + vibrate(service, effect); + assertFalse(service.isVibrating()); + + verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any()); + verify(mIInputManagerMock).vibrate(eq(2), eq(effect), any()); + verify(mNativeWrapperMock, never()).vibratorPerformComposedEffect(any(), anyLong()); + } + @Test public void vibrate_withWaveform_controlsVibratorAmplitudeDuringTotalVibrationTime() throws Exception { @@ -499,6 +571,25 @@ public class VibratorServiceTest { delay < maxDelay); } + @Test + public void vibrate_withWaveformAndInputDevices_vibratesInputDevices() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(1)).thenReturn(createInputDeviceWithVibrator(1)); + setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1); + VibratorService service = createService(); + Mockito.clearInvocations(mNativeWrapperMock); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10, 10, 10}, new int[]{100, 200, 50}, -1); + vibrate(service, effect); + assertFalse(service.isVibrating()); + + // Wait for VibrateThread to turn input device vibrator ON. + Thread.sleep(5); + verify(mIInputManagerMock).vibrate(eq(1), eq(effect), any()); + verify(mNativeWrapperMock, never()).vibratorOn(anyLong(), anyLong()); + } + @Test public void vibrate_withOneShotAndNativeCallbackTriggered_finishesVibration() { VibratorService service = createService(); @@ -795,6 +886,11 @@ public class VibratorServiceTest { when(mNativeWrapperMock.vibratorGetCapabilities()).thenReturn((long) capabilities); } + private InputDevice createInputDeviceWithVibrator(int id) { + return new InputDevice(id, 0, 0, "name", 0, 0, "description", false, 0, 0, + null, /* hasVibrator= */ true, false, false); + } + private static void addLocalServiceMock(Class clazz, T mock) { LocalServices.removeServiceForTest(clazz); LocalServices.addService(clazz, mock); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java new file mode 100644 index 0000000000000..fa8e36741bccd --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/vibrator/InputDeviceDelegateTest.java @@ -0,0 +1,291 @@ +/* + * 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.vibrator; + +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.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.input.IInputDevicesChangedListener; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; +import android.os.Handler; +import android.os.Process; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; +import android.view.InputDevice; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Tests for {@link InputDeviceDelegate}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:InputDeviceDelegateTest + */ +@Presubmit +public class InputDeviceDelegateTest { + + private static final int UID = Process.ROOT_UID; + private static final String PACKAGE_NAME = "package"; + private static final String REASON = "some reason"; + private static final VibrationAttributes VIBRATION_ATTRIBUTES = + new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build(); + + @Rule public MockitoRule rule = MockitoJUnit.rule(); + + @Mock private IInputManager mIInputManagerMock; + + private TestLooper mTestLooper; + private ContextWrapper mContextSpy; + private InputDeviceDelegate mInputDeviceDelegate; + private IInputDevicesChangedListener mIInputDevicesChangedListener; + + @Before + public void setUp() throws Exception { + mTestLooper = new TestLooper(); + mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); + InputManager inputManager = InputManager.resetInstance(mIInputManagerMock); + + when(mContextSpy.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager); + doAnswer(invocation -> mIInputDevicesChangedListener = invocation.getArgument(0)) + .when(mIInputManagerMock).registerInputDevicesChangedListener(any()); + + mInputDeviceDelegate = new InputDeviceDelegate( + mContextSpy, new Handler(mTestLooper.getLooper())); + } + + @Test + public void onInputDeviceAdded_withSettingsDisabled_ignoresNewDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ false); + assertFalse(mInputDeviceDelegate.isAvailable()); + + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + mInputDeviceDelegate.onInputDeviceAdded(1); + + assertFalse(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock, never()).getInputDevice(anyInt()); + } + + @Test + public void onInputDeviceAdded_withDeviceWithoutVibrator_ignoresNewDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertFalse(mInputDeviceDelegate.isAvailable()); + + when(mIInputManagerMock.getInputDevice(eq(1))) + .thenReturn(createInputDeviceWithoutVibrator(1)); + updateInputDevices(new int[]{1}); + + assertFalse(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock).getInputDevice(eq(1)); + } + + @Test + public void onInputDeviceAdded_withDeviceWithVibrator_addsNewDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertFalse(mInputDeviceDelegate.isAvailable()); + + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + updateInputDevices(new int[]{1}); + + assertTrue(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock).getInputDevice(eq(1)); + } + + @Test + public void onInputDeviceChanged_withSettingsDisabled_ignoresDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ false); + + updateInputDevices(new int[]{1}); + assertFalse(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock, never()).getInputDevice(anyInt()); + } + + @Test + public void onInputDeviceChanged_deviceLosesVibrator_removesDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(eq(1))) + .thenReturn(createInputDeviceWithVibrator(1), createInputDeviceWithoutVibrator(1)); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertTrue(mInputDeviceDelegate.isAvailable()); + + updateInputDevices(new int[]{1}); + assertFalse(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock, times(2)).getInputDevice(eq(1)); + } + + @Test + public void onInputDeviceChanged_deviceLost_removesDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(eq(1))) + .thenReturn(createInputDeviceWithVibrator(1), (InputDevice) null); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertTrue(mInputDeviceDelegate.isAvailable()); + + updateInputDevices(new int[]{1}); + assertFalse(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock, times(2)).getInputDevice(eq(1)); + } + + @Test + public void onInputDeviceChanged_deviceAddsVibrator_addsDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(eq(1))) + .thenReturn(createInputDeviceWithoutVibrator(1), createInputDeviceWithVibrator(1)); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertFalse(mInputDeviceDelegate.isAvailable()); + + updateInputDevices(new int[]{1}); + assertTrue(mInputDeviceDelegate.isAvailable()); + verify(mIInputManagerMock, times(2)).getInputDevice(eq(1)); + } + + @Test + public void onInputDeviceRemoved_removesDevice() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2}); + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn( + createInputDeviceWithoutVibrator(1)); + when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2)); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertTrue(mInputDeviceDelegate.isAvailable()); + + updateInputDevices(new int[]{1}); + assertFalse(mInputDeviceDelegate.isAvailable()); + } + + @Test + public void updateInputDeviceVibrators_usesFlagToLoadDeviceList() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2}); + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2)); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertTrue(mInputDeviceDelegate.isAvailable()); + + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ false); + assertFalse(mInputDeviceDelegate.isAvailable()); + } + + @Test + public void updateInputDeviceVibrators_withDeviceWithoutVibrator_deviceIsIgnored() + throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1}); + when(mIInputManagerMock.getInputDevice(eq(1))) + .thenReturn(createInputDeviceWithoutVibrator(1)); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + assertFalse(mInputDeviceDelegate.isAvailable()); + } + + @Test + public void vibrateIfAvailable_withNoInputDevice_returnsFalse() { + VibrationEffect effect = VibrationEffect.createOneShot(100, 255); + assertFalse(mInputDeviceDelegate.isAvailable()); + assertFalse(mInputDeviceDelegate.vibrateIfAvailable( + UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES)); + } + + @Test + public void vibrateIfAvailable_withInputDevices_returnsTrueAndVibratesAllDevices() + throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2}); + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2)); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + + VibrationEffect effect = VibrationEffect.createOneShot(100, 255); + assertTrue(mInputDeviceDelegate.vibrateIfAvailable( + UID, PACKAGE_NAME, effect, REASON, VIBRATION_ATTRIBUTES)); + verify(mIInputManagerMock).vibrate(eq(1), same(effect), any()); + verify(mIInputManagerMock).vibrate(eq(2), same(effect), any()); + } + + @Test + public void cancelVibrateIfAvailable_withNoInputDevice_returnsFalse() throws Exception { + assertFalse(mInputDeviceDelegate.isAvailable()); + assertFalse(mInputDeviceDelegate.cancelVibrateIfAvailable()); + verify(mIInputManagerMock, never()).cancelVibrate(anyInt(), any()); + } + + @Test + public void cancelVibrateIfAvailable_withInputDevices_returnsTrueAndStopsAllDevices() + throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{1, 2}); + when(mIInputManagerMock.getInputDevice(eq(1))).thenReturn(createInputDeviceWithVibrator(1)); + when(mIInputManagerMock.getInputDevice(eq(2))).thenReturn(createInputDeviceWithVibrator(2)); + mInputDeviceDelegate.updateInputDeviceVibrators(/* vibrateInputDevices= */ true); + + assertTrue(mInputDeviceDelegate.isAvailable()); + assertTrue(mInputDeviceDelegate.cancelVibrateIfAvailable()); + verify(mIInputManagerMock).cancelVibrate(eq(1), any()); + verify(mIInputManagerMock).cancelVibrate(eq(2), any()); + } + + private void updateInputDevices(int[] deviceIds) throws Exception { + int[] deviceIdsAndGenerations = new int[deviceIds.length * 2]; + for (int i = 0; i < deviceIdsAndGenerations.length; i += 2) { + deviceIdsAndGenerations[i] = deviceIds[i / 2]; + deviceIdsAndGenerations[i + 1] = 2; // update by increasing it's generation to 2. + } + // Force initialization of mIInputDevicesChangedListener, if it still haven't + InputManager.getInstance().getInputDeviceIds(); + mIInputDevicesChangedListener.onInputDevicesChanged(deviceIdsAndGenerations); + // Makes sure all callbacks from InputDeviceDelegate are executed. + mTestLooper.dispatchAll(); + } + + private InputDevice createInputDeviceWithVibrator(int id) { + return createInputDevice(id, /* hasVibrator= */ true); + } + + private InputDevice createInputDeviceWithoutVibrator(int id) { + return createInputDevice(id, /* hasVibrator= */ false); + } + + private InputDevice createInputDevice(int id, boolean hasVibrator) { + return new InputDevice(id, 0, 0, "name", 0, 0, "description", false, 0, 0, + null, hasVibrator, false, false); + } +}