blob: e186f2c19e9ca63031b6fd05aaacd1983329a914 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package arc
import (
"context"
"encoding/json"
"math"
"chromiumos/tast/errors"
"chromiumos/tast/local/android/ui"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/input"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: Gamepad,
Desc: "Checks gamepad support works on Android",
Contacts: []string{"tetsui@chromium.org", "arc-framework+tast@google.com"},
Attr: []string{"group:mainline"},
SoftwareDeps: []string{"chrome"},
Fixture: "arcBooted",
Params: []testing.Param{{
ExtraSoftwareDeps: []string{"android_p"},
}, {
Name: "vm",
ExtraSoftwareDeps: []string{"android_vm"},
}},
})
}
type inputDevice struct {
DeviceID int `json:"device_id"`
ProductID uint16 `json:"product_id"`
VendorID uint16 `json:"vendor_id"`
Name string `json:"name"`
MotionRanges map[string]struct {
Flat float64 `json:"flat"`
Fuzz float64 `json:"fuzz"`
Max float64 `json:"max"`
Min float64 `json:"min"`
Resolution float64 `json:"resolution"`
Source int `json:"source"`
} `json:"motion_ranges"`
}
// verifyGamepadDeviceInfo confirms the gamepad's InputDevice information is correct.
func verifyGamepadDeviceInfo(s *testing.State, gp *input.GamepadEventWriter, d *inputDevice) {
// DeviceID may change at runtime.
if d.ProductID != gp.ProductID() {
s.Errorf("product ID doesn't match: got %v; want %v", d.ProductID, gp.ProductID())
}
if d.VendorID != gp.VendorID() {
s.Errorf("vendor ID doesn't match: got %v; want %v", d.VendorID, gp.VendorID())
}
isCenteredAxis := map[string]bool{
"AXIS_X": true,
"AXIS_Y": true,
"AXIS_Z": true,
"AXIS_RZ": true,
"AXIS_HAT_X": true,
"AXIS_HAT_Y": true,
}
gamepadMapping := map[input.EventCode]string{
input.ABS_X: "AXIS_X",
input.ABS_Y: "AXIS_Y",
input.ABS_RX: "AXIS_Z",
input.ABS_RY: "AXIS_RZ",
input.ABS_HAT0X: "AXIS_HAT_X",
input.ABS_HAT0Y: "AXIS_HAT_Y",
input.ABS_Z: "AXIS_LTRIGGER",
input.ABS_RZ: "AXIS_RTRIGGER",
}
almostEqual := func(a, b float64) bool {
return math.Abs(a-b) <= 1e-5
}
for code, info := range gp.Axes() {
axisName, ok := gamepadMapping[code]
if !ok {
continue
}
expectedMin := 0.0
expectedMax := 1.0
if isCenteredAxis[axisName] {
expectedMin = -1.0
expectedMax = 1.0
}
scale := (expectedMax - expectedMin) / float64(info.Maximum-info.Minimum)
expectedFuzz := float64(info.Fuzz) * scale
expectedFlat := float64(info.Flat) * scale
motionRange, ok := d.MotionRanges[axisName]
if !ok {
s.Errorf("gamepad axis %s not found", axisName)
}
if !almostEqual(expectedMin, motionRange.Min) {
s.Errorf("min does not match for %s: got %f; want %f", axisName, motionRange.Min, expectedMin)
}
if !almostEqual(expectedMax, motionRange.Max) {
s.Errorf("max does not match for %s: got %f; want %f", axisName, motionRange.Max, expectedMax)
}
if !almostEqual(expectedFuzz, motionRange.Fuzz) {
s.Errorf("fuzz does not match for %s: got %f; want %f", axisName, motionRange.Fuzz, expectedFuzz)
}
if !almostEqual(expectedFlat, motionRange.Flat) {
s.Errorf("flat does not match for %s: got %f; want %f", axisName, motionRange.Flat, expectedFlat)
}
}
return
}
type gamepadKeyEvent struct {
Action string `json:"action"`
KeyCode string `json:"key_code"`
DeviceID int `json:"device_id"`
}
type gamepadMotionEvent struct {
Action string `json:"action"`
Axes map[string]float64 `json:"axes"`
}
// getInputDevices gets the input device information saved in app UI.
func getInputDevices(ctx context.Context, d *ui.Device) ([]inputDevice, error) {
view := d.Object(ui.ID("org.chromium.arc.testapp.gamepad:id/device_status"))
text, err := view.GetText(ctx)
if err != nil {
return nil, err
}
var devices []inputDevice
if err := json.Unmarshal([]byte(text), &devices); err != nil {
return nil, err
}
return devices, nil
}
// getGamepadKeyEvents gets all key events saved in app UI.
func getGamepadKeyEvents(ctx context.Context, d *ui.Device) ([]gamepadKeyEvent, error) {
view := d.Object(ui.ID("org.chromium.arc.testapp.gamepad:id/key_events"))
text, err := view.GetText(ctx)
if err != nil {
return nil, err
}
var events []gamepadKeyEvent
if err := json.Unmarshal([]byte(text), &events); err != nil {
return nil, err
}
return events, nil
}
// waitForKeyEvents waits until the app receives expected events and the length of received events is same as expected events.
func waitForKeyEvents(ctx context.Context, d *ui.Device, deviceID int, expects []gamepadKeyEvent) error {
return testing.Poll(ctx, func(ctx context.Context) error {
events, err := getGamepadKeyEvents(ctx, d)
if err != nil {
return err
} else if len(events) != len(expects) {
return errors.Errorf("unexpected number of gamepad key events: got %d; want %d", len(events), len(expects))
} else {
for i, expect := range expects {
// DeviceID may change at runtime.
expect.DeviceID = deviceID
if expect != events[i] {
return errors.Errorf("unexpected gamepad event: got %v; want %v", events[i], expect)
}
}
return nil
}
}, nil)
}
// getGamepadMotionEvent gets the motion event saved in app UI.
func getGamepadMotionEvent(ctx context.Context, d *ui.Device) (*gamepadMotionEvent, error) {
view := d.Object(ui.ID("org.chromium.arc.testapp.gamepad:id/motion_event"))
text, err := view.GetText(ctx)
if err != nil {
return nil, err
}
var event gamepadMotionEvent
if err := json.Unmarshal([]byte(text), &event); err != nil {
return nil, err
}
return &event, nil
}
// waitForGamepadAxis waits until the app receives expected motion event.
func waitForGamepadAxis(ctx context.Context, d *ui.Device, axis string, expected float64) error {
return testing.Poll(ctx, func(ctx context.Context) error {
if event, err := getGamepadMotionEvent(ctx, d); err != nil {
return err
} else if val, ok := event.Axes[axis]; !ok || val != expected {
return errors.Errorf("unexpected %s value: got %f; want %f", axis, val, expected)
}
return nil
}, nil)
}
func Gamepad(ctx context.Context, s *testing.State) {
a := s.FixtValue().(*arc.PreData).ARC
cr := s.FixtValue().(*arc.PreData).Chrome
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create Test API connection: ", err)
}
d, err := a.NewUIDevice(ctx)
if err != nil {
s.Fatal("Failed initializing UI Automator: ", err)
}
defer d.Close(ctx)
gp, err := input.Gamepad(ctx)
if err != nil {
s.Fatal("Failed to create a gamepad: ", err)
}
defer func() {
if gp != nil {
gp.Close()
}
}()
s.Log("Created a virtual gamepad device ", gp.Device())
const (
apk = "ArcGamepadTest.apk"
pkg = "org.chromium.arc.testapp.gamepad"
cls = "org.chromium.arc.testapp.gamepad.MainActivity"
)
s.Log("Installing app")
if err := a.Install(ctx, arc.APKPath(apk)); err != nil {
s.Fatal("Failed installing app: ", err)
}
s.Log("Starting app")
act, err := arc.NewActivity(a, pkg, cls)
if err != nil {
s.Fatal("Failed to create a new activity: ", err)
}
defer act.Close()
if err := act.Start(ctx, tconn); err != nil {
s.Fatal("Failed to start the activity: ", err)
}
defer act.Close()
s.Log("Checking the device connection")
var device inputDevice
if err := testing.Poll(ctx, func(ctx context.Context) error {
devices, err := getInputDevices(ctx, d)
if err != nil {
return err
} else if len(devices) != 1 {
return errors.Errorf("unexpected number of gamepad devices: got %v; want 1",
len(devices))
}
device = devices[0]
return nil
}, nil); err != nil {
s.Fatal("Cannot get the gamepad device: ", err)
}
verifyGamepadDeviceInfo(s, gp, &device)
s.Log("Pressing buttons")
if err := gp.TapButton(ctx, input.BTN_EAST); err != nil {
s.Fatal("Failed to press button: ", err)
}
if err := gp.TapButton(ctx, input.BTN_SOUTH); err != nil {
s.Fatal("Failed to press button: ", err)
}
const (
ActionDown = "ACTION_DOWN"
ActionUp = "ACTION_UP"
KeycodeButtonA = "KEYCODE_BUTTON_A"
KeycodeButtonX = "KEYCODE_BUTTON_X"
AxisX = "AXIS_X"
)
expectedEvents := []gamepadKeyEvent{
{Action: ActionDown, KeyCode: KeycodeButtonA},
{Action: ActionUp, KeyCode: KeycodeButtonA},
{Action: ActionDown, KeyCode: KeycodeButtonX},
{Action: ActionUp, KeyCode: KeycodeButtonX}}
s.Log("Checking the generated gamepad events")
if err := waitForKeyEvents(ctx, d, device.DeviceID, expectedEvents); err != nil {
s.Fatal("Failed to wait for key events: ", err)
}
s.Log("Moving the analog stick")
axis := gp.Axes()[input.ABS_X]
if err := gp.MoveAxis(ctx, input.ABS_X, axis.Maximum); err != nil {
s.Fatal("Failed to move axis: ", err)
}
if err := waitForGamepadAxis(ctx, d, AxisX, 1.0); err != nil {
s.Fatal("Failed to wait for axis change: ", err)
}
if err := gp.MoveAxis(ctx, input.ABS_X, axis.Minimum); err != nil {
s.Fatal("Failed to move axis: ", err)
}
if err := waitForGamepadAxis(ctx, d, AxisX, -1.0); err != nil {
s.Fatal("Failed to wait for axis change: ", err)
}
// Press button and move axis together, and then release button.
s.Log("Pressing button and moving the analog stick together")
injectEvents := []input.GamepadEvent{
{Et: input.EV_ABS, Ec: input.ABS_X, Val: axis.Maximum},
{Et: input.EV_KEY, Ec: input.BTN_EAST, Val: 1}}
if err := gp.PressButtonsAndAxes(ctx, injectEvents); err != nil {
s.Fatal("Failed to press button and move axis together: ", err)
}
// Release button.
if err := gp.ReleaseButton(ctx, input.BTN_EAST); err != nil {
s.Fatal("Failed to release button: ", err)
}
expectedEvents = append(expectedEvents, gamepadKeyEvent{Action: ActionDown, KeyCode: KeycodeButtonA})
expectedEvents = append(expectedEvents, gamepadKeyEvent{Action: ActionUp, KeyCode: KeycodeButtonA})
if err := waitForKeyEvents(ctx, d, device.DeviceID, expectedEvents); err != nil {
s.Fatal("Failed to wait for key event change: ", err)
}
if err := waitForGamepadAxis(ctx, d, AxisX, 1.0); err != nil {
s.Fatal("Failed to wait for axis change: ", err)
}
s.Log("Disconnecting the gamepad")
if err := gp.Close(); err != nil {
s.Fatal("Failed to close the gamepad: ", err)
}
gp = nil
if err := testing.Poll(ctx, func(ctx context.Context) error {
if devices, err := getInputDevices(ctx, d); err != nil {
return err
} else if len(devices) > 0 {
return errors.Errorf("the gamepad device still exist: %+v", devices)
}
return nil
}, nil); err != nil {
s.Fatal("Failed to disconnect the gamepad: ", err)
}
}