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); + } +}