| /* |
| * Copyright 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 android.hardware.input.cts.tests; |
| |
| import static android.hardware.lights.LightsRequest.Builder; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.timeout; |
| import static org.mockito.Mockito.verify; |
| |
| import android.hardware.BatteryState; |
| import android.hardware.input.InputManager; |
| import android.hardware.input.cts.GlobalKeyMapping; |
| import android.hardware.lights.Light; |
| import android.hardware.lights.LightState; |
| import android.hardware.lights.LightsManager; |
| import android.os.SystemClock; |
| import android.os.VibrationEffect; |
| import android.os.Vibrator; |
| import android.os.Vibrator.OnVibratorStateChangedListener; |
| import android.os.VintfRuntimeInfo; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.InputDevice; |
| |
| import androidx.annotation.CallSuper; |
| |
| import com.android.cts.input.HidBatteryTestData; |
| import com.android.cts.input.HidDevice; |
| import com.android.cts.input.HidLightTestData; |
| import com.android.cts.input.HidResultData; |
| import com.android.cts.input.HidTestData; |
| import com.android.cts.input.HidVibratorTestData; |
| |
| import org.junit.Rule; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnit; |
| import org.mockito.junit.MockitoRule; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| public class InputHidTestCase extends InputTestCase { |
| private static final String TAG = "InputHidTestCase"; |
| // Sync with linux uhid_event_type::UHID_OUTPUT |
| private static final byte UHID_EVENT_TYPE_UHID_OUTPUT = 6; |
| private static final long CALLBACK_TIMEOUT_MILLIS = 5000; |
| |
| private HidDevice mHidDevice; |
| private final GlobalKeyMapping mGlobalKeyMapping = new GlobalKeyMapping( |
| mInstrumentation.getTargetContext()); |
| private int mDeviceId; |
| private final int mRegisterResourceId; |
| private boolean mDelayAfterSetup = false; |
| |
| @Rule |
| public MockitoRule rule = MockitoJUnit.rule(); |
| @Mock |
| private OnVibratorStateChangedListener mListener; |
| |
| InputHidTestCase(int registerResourceId) { |
| super(registerResourceId); |
| mRegisterResourceId = registerResourceId; |
| } |
| |
| @CallSuper |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| // Even though we already wait for all possible callbacks such as UHID_START and UHID_OPEN, |
| // and wait for the correct device to appear by specifying expected source type in the |
| // register command, some devices, perhaps due to splitting, do not produce events as soon |
| // as they are created. Adding a small delay resolves this issue. |
| if (mDelayAfterSetup) { |
| SystemClock.sleep(1000); |
| } |
| } |
| |
| protected void addDelayAfterSetup() { |
| mDelayAfterSetup = true; |
| } |
| |
| /** Check if input device has specific capability */ |
| interface Capability { |
| boolean check(InputDevice inputDevice); |
| } |
| |
| private static Pair<Integer, Integer> getVersionFromString(String version) { |
| // Only gets major and minor number of the version string. |
| final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*"); |
| final Matcher m = versionPattern.matcher(version); |
| if (m.matches()) { |
| final int major = Integer.parseInt(m.group(1)); |
| final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3)); |
| |
| return new Pair<>(major, minor); |
| |
| } else { |
| fail("Cannot parse kernel version: " + version); |
| return new Pair<>(0, 0); |
| } |
| } |
| |
| private static int compareMajorMinorVersion(final String s1, final String s2) throws Exception { |
| final Pair<Integer, Integer> v1 = getVersionFromString(s1); |
| final Pair<Integer, Integer> v2 = getVersionFromString(s2); |
| |
| if (v1.first == v2.first) { |
| return Integer.compare(v1.second, v2.second); |
| } else { |
| return Integer.compare(v1.first, v2.first); |
| } |
| } |
| |
| protected static boolean isKernelVersionGreaterThan(String version) throws Exception { |
| final String actualVersion = VintfRuntimeInfo.getKernelVersion(); |
| return compareMajorMinorVersion(actualVersion, version) > 0; |
| } |
| |
| /** Gets an input device with specific capability */ |
| private InputDevice getInputDevice(Capability capability) { |
| final InputManager inputManager = |
| mInstrumentation.getTargetContext().getSystemService(InputManager.class); |
| final int[] inputDeviceIds = inputManager.getInputDeviceIds(); |
| for (int inputDeviceId : inputDeviceIds) { |
| final InputDevice inputDevice = inputManager.getInputDevice(inputDeviceId); |
| if (inputDevice.getVendorId() == mVid && inputDevice.getProductId() == mPid |
| && capability.check(inputDevice)) { |
| return inputDevice; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Gets a vibrator from input device with specified Vendor Id and Product Id |
| * from device registration command. |
| * @return Vibrator object in specified InputDevice |
| */ |
| private Vibrator getVibrator() { |
| InputDevice inputDevice = getInputDevice((d) -> d.getVibrator().hasVibrator()); |
| if (inputDevice == null) { |
| fail("Failed to find test device with vibrator"); |
| } |
| return inputDevice.getVibrator(); |
| } |
| |
| /** |
| * Gets a battery from input device with specified Vendor Id and Product Id |
| * from device registration command. |
| * @return Battery object in specified InputDevice |
| */ |
| private BatteryState getBatteryState() { |
| InputDevice inputDevice = getInputDevice((d) -> d.getBatteryState().isPresent()); |
| if (inputDevice == null) { |
| fail("Failed to find test device with battery"); |
| } |
| return inputDevice.getBatteryState(); |
| } |
| |
| /** |
| * Gets a light manager object from input device with specified Vendor Id and Product Id |
| * from device registration command. |
| * @return LightsManager object in specified InputDevice |
| */ |
| private LightsManager getLightsManager() { |
| InputDevice inputDevice = getInputDevice( |
| (d) -> !d.getLightsManager().getLights().isEmpty()); |
| if (inputDevice == null) { |
| fail("Failed to find test device with light"); |
| } |
| return inputDevice.getLightsManager(); |
| } |
| |
| @Override |
| protected void setUpDevice(int id, int vendorId, int productId, int sources, |
| String registerCommand) { |
| mDeviceId = id; |
| mHidDevice = new HidDevice(mInstrumentation, id, vendorId, productId, sources, |
| registerCommand); |
| assertNotNull(mHidDevice); |
| } |
| |
| @Override |
| protected void tearDownDevice() { |
| if (mHidDevice != null) { |
| mHidDevice.close(); |
| } |
| } |
| |
| @Override |
| protected void testInputDeviceEvents(int resourceId) { |
| List<HidTestData> tests = mParser.getHidTestData(resourceId); |
| // Global keys are handled by the framework and do not reach apps. |
| // The set of global keys is vendor-specific. |
| // Remove tests which contain global keys because we can't test them |
| tests.removeIf(testData -> testData.events.removeIf(mGlobalKeyMapping::isGlobalKey)); |
| |
| for (HidTestData testData: tests) { |
| mCurrentTestCase = testData.name; |
| // Send all of the HID reports |
| for (int i = 0; i < testData.reports.size(); i++) { |
| final String report = testData.reports.get(i); |
| mHidDevice.sendHidReport(report); |
| } |
| verifyEvents(testData.events); |
| } |
| } |
| |
| private boolean verifyVibratorReportData(HidVibratorTestData test, HidResultData result) { |
| for (Map.Entry<Integer, Integer> entry : test.verifyMap.entrySet()) { |
| final int index = entry.getKey(); |
| final int value = entry.getValue(); |
| if ((result.reportData[index] & 0XFF) != value) { |
| Log.v(TAG, "index=" + index + " value= " + value |
| + "actual= " + (result.reportData[index] & 0XFF)); |
| return false; |
| } |
| } |
| final int ffLeft = result.reportData[test.leftFfIndex] & 0xFF; |
| final int ffRight = result.reportData[test.rightFfIndex] & 0xFF; |
| |
| return ffLeft > 0 && ffRight > 0; |
| } |
| |
| public void testInputVibratorEvents(int resourceId) throws Exception { |
| final List<HidVibratorTestData> tests = mParser.getHidVibratorTestData(resourceId); |
| |
| for (HidVibratorTestData test : tests) { |
| assertEquals(test.durations.size(), test.amplitudes.size()); |
| assertTrue(test.durations.size() > 0); |
| |
| final long timeoutMills; |
| final long totalVibrations = test.durations.size(); |
| final VibrationEffect effect; |
| if (test.durations.size() == 1) { |
| long duration = test.durations.get(0); |
| int amplitude = test.amplitudes.get(0); |
| effect = VibrationEffect.createOneShot(duration, amplitude); |
| // Set timeout to be 2 times of the effect duration. |
| timeoutMills = duration * 2; |
| } else { |
| long[] durations = test.durations.stream().mapToLong(Long::longValue).toArray(); |
| int[] amplitudes = test.amplitudes.stream().mapToInt(Integer::intValue).toArray(); |
| effect = VibrationEffect.createWaveform( |
| durations, amplitudes, -1); |
| // Set timeout to be 2 times of the effect total duration. |
| timeoutMills = Arrays.stream(durations).sum() * 2; |
| } |
| |
| final Vibrator vibrator = getVibrator(); |
| assertNotNull(vibrator); |
| vibrator.addVibratorStateListener(mListener); |
| verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS) |
| .times(1)).onVibratorStateChanged(false); |
| reset(mListener); |
| // Start vibration |
| vibrator.vibrate(effect); |
| // Verify vibrator state listener |
| verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS) |
| .times(1)).onVibratorStateChanged(true); |
| assertTrue(vibrator.isVibrating()); |
| |
| final long startTime = SystemClock.elapsedRealtime(); |
| List<HidResultData> results = new ArrayList<>(); |
| int vibrationCount = 0; |
| // Check the vibration ffLeft and ffRight amplitude to be expected. |
| while (vibrationCount < totalVibrations |
| && SystemClock.elapsedRealtime() - startTime < timeoutMills) { |
| SystemClock.sleep(1000); |
| |
| results = mHidDevice.getResults(mDeviceId, UHID_EVENT_TYPE_UHID_OUTPUT); |
| if (results.size() < totalVibrations) { |
| continue; |
| } |
| vibrationCount = 0; |
| for (int i = 0; i < results.size(); i++) { |
| HidResultData result = results.get(i); |
| if (result.deviceId == mDeviceId |
| && verifyVibratorReportData(test, result)) { |
| int ffLeft = result.reportData[test.leftFfIndex] & 0xFF; |
| int ffRight = result.reportData[test.rightFfIndex] & 0xFF; |
| Log.v(TAG, "eventId=" + result.eventId + " reportType=" |
| + result.reportType + " left=" + ffLeft + " right=" + ffRight); |
| // Check the amplitudes of FF effect are expected. |
| if (ffLeft == test.amplitudes.get(vibrationCount) |
| && ffRight == test.amplitudes.get(vibrationCount)) { |
| vibrationCount++; |
| } |
| } |
| } |
| } |
| assertEquals(vibrationCount, totalVibrations); |
| // Verify vibrator state listener |
| verify(mListener, timeout(CALLBACK_TIMEOUT_MILLIS) |
| .times(1)).onVibratorStateChanged(false); |
| assertFalse(vibrator.isVibrating()); |
| vibrator.removeVibratorStateListener(mListener); |
| reset(mListener); |
| } |
| } |
| |
| public void testInputVibratorManagerEvents(int resourceId) throws Exception { |
| final List<HidVibratorTestData> tests = mParser.getHidVibratorTestData(resourceId); |
| |
| for (HidVibratorTestData test : tests) { |
| assertEquals(test.durations.size(), test.amplitudes.size()); |
| assertTrue(test.durations.size() > 0); |
| |
| final long timeoutMills; |
| final long totalVibrations = test.durations.size(); |
| final VibrationEffect effect; |
| if (test.durations.size() == 1) { |
| long duration = test.durations.get(0); |
| int amplitude = test.amplitudes.get(0); |
| effect = VibrationEffect.createOneShot(duration, amplitude); |
| // Set timeout to be 2 times of the effect duration. |
| timeoutMills = duration * 2; |
| } else { |
| long[] durations = test.durations.stream().mapToLong(Long::longValue).toArray(); |
| int[] amplitudes = test.amplitudes.stream().mapToInt(Integer::intValue).toArray(); |
| effect = VibrationEffect.createWaveform( |
| durations, amplitudes, -1); |
| // Set timeout to be 2 times of the effect total duration. |
| timeoutMills = Arrays.stream(durations).sum() * 2; |
| } |
| |
| final Vibrator vibrator = getVibrator(); |
| assertNotNull(vibrator); |
| // Start vibration |
| vibrator.vibrate(effect); |
| final long startTime = SystemClock.elapsedRealtime(); |
| List<HidResultData> results = new ArrayList<>(); |
| int vibrationCount = 0; |
| // Check the vibration ffLeft and ffRight amplitude to be expected. |
| while (vibrationCount < totalVibrations |
| && SystemClock.elapsedRealtime() - startTime < timeoutMills) { |
| SystemClock.sleep(1000); |
| |
| results = mHidDevice.getResults(mDeviceId, UHID_EVENT_TYPE_UHID_OUTPUT); |
| if (results.size() < totalVibrations) { |
| continue; |
| } |
| vibrationCount = 0; |
| for (int i = 0; i < results.size(); i++) { |
| HidResultData result = results.get(i); |
| if (result.deviceId == mDeviceId |
| && verifyVibratorReportData(test, result)) { |
| int ffLeft = result.reportData[test.leftFfIndex] & 0xFF; |
| int ffRight = result.reportData[test.rightFfIndex] & 0xFF; |
| Log.v(TAG, "eventId=" + result.eventId + " reportType=" |
| + result.reportType + " left=" + ffLeft + " right=" + ffRight); |
| // Check the amplitudes of FF effect are expected. |
| if (ffLeft == test.amplitudes.get(vibrationCount) |
| && ffRight == test.amplitudes.get(vibrationCount)) { |
| vibrationCount++; |
| } |
| } |
| } |
| } |
| assertEquals(vibrationCount, totalVibrations); |
| } |
| } |
| |
| public void testInputBatteryEvents(int resourceId) { |
| final BatteryState batteryState = getBatteryState(); |
| assertNotNull(batteryState); |
| |
| final List<HidBatteryTestData> tests = mParser.getHidBatteryTestData(resourceId); |
| for (HidBatteryTestData testData : tests) { |
| |
| // Send all of the HID reports |
| for (int i = 0; i < testData.reports.size(); i++) { |
| final String report = testData.reports.get(i); |
| mHidDevice.sendHidReport(report); |
| } |
| // Wait for power_supply sysfs node get updated. |
| SystemClock.sleep(100); |
| float capacity = batteryState.getCapacity(); |
| int status = batteryState.getStatus(); |
| assertEquals("Test: " + testData.name, testData.status, status); |
| boolean capacityMatch = false; |
| for (int i = 0; i < testData.capacities.length; i++) { |
| if (capacity == testData.capacities[i]) { |
| capacityMatch = true; |
| break; |
| } |
| } |
| assertTrue("Test: " + testData.name + " capacity " + capacity + " expect " |
| + Arrays.toString(testData.capacities), capacityMatch); |
| } |
| } |
| |
| public void testInputLightsManager(int resourceId) throws Exception { |
| final LightsManager lightsManager = getLightsManager(); |
| final List<Light> lights = lightsManager.getLights(); |
| |
| final List<HidLightTestData> tests = mParser.getHidLightTestData(resourceId); |
| for (HidLightTestData test : tests) { |
| Light light = null; |
| for (int i = 0; i < lights.size(); i++) { |
| if (lights.get(i).getType() == test.lightType |
| && test.lightName.equals(lights.get(i).getName())) { |
| light = lights.get(i); |
| } |
| } |
| assertNotNull("Light type " + test.lightType + " name " + test.lightName |
| + " does not exist. Lights found: " + lights, light); |
| try (LightsManager.LightsSession session = lightsManager.openSession()) { |
| // Can't set both player id and color in same LightState |
| assertFalse(test.lightColor > 0 && test.lightPlayerId > 0); |
| // Issue the session requests to turn single light on |
| if (test.lightPlayerId > 0) { |
| session.requestLights(new Builder() |
| .addLight(light, (new LightState.Builder()) |
| .setPlayerId(test.lightPlayerId).build()).build()); |
| } else { |
| session.requestLights(new Builder() |
| .addLight(light, (new LightState.Builder()).setColor(test.lightColor) |
| .build()).build()); |
| } |
| // Some devices (e.g. Sixaxis) defer sending output packets until they've seen at |
| // least one input packet. |
| if (!test.report.isEmpty()) { |
| mHidDevice.sendHidReport(test.report); |
| } |
| // Delay before sysfs node was updated. |
| SystemClock.sleep(200); |
| // Verify HID report data |
| List<HidResultData> results = mHidDevice.getResults(mDeviceId, |
| test.hidEventType); |
| assertFalse(results.isEmpty()); |
| // We just check the last HID output to be expected. |
| HidResultData result = results.get(results.size() - 1); |
| for (Map.Entry<Integer, Integer> entry : test.expectedHidData.entrySet()) { |
| final int index = entry.getKey(); |
| final int value = entry.getValue(); |
| int actual = result.reportData[index] & 0xFF; |
| assertEquals("Led data index " + index, value, actual); |
| |
| } |
| |
| // Then the light state should be what we requested. |
| if (test.lightPlayerId > 0) { |
| assertThat(lightsManager.getLightState(light).getPlayerId()) |
| .isEqualTo(test.lightPlayerId); |
| } else { |
| assertThat(lightsManager.getLightState(light).getColor()) |
| .isEqualTo(test.lightColor); |
| } |
| } |
| } |
| } |
| |
| } |