blob: e73a06c5435a7730f5c19b7b3b063b32b4e174cb [file] [log] [blame]
// Copyright 2018 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 input
import (
"context"
"fmt"
"math"
"math/big"
"os"
"time"
"unsafe"
"chromiumos/tast/errors"
"chromiumos/tast/local/coords"
"chromiumos/tast/testing"
)
// TouchCoord describes an X or Y coordinate in touchscreen coordinates
// (rather than pixels).
type TouchCoord int32
// TouchscreenEventWriter supports injecting touch events into a touchscreen device.
// It supports multitouch as defined in "Protocol Example B" here:
// https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt
// https://www.kernel.org/doc/Documentation/input/event-codes.txt
// This is partial implementation of the multi-touch specification. Each injected
// touch event contains the following codes:
// - ABS_MT_TRACKING_ID
// - ABS_MT_POSITION_X & ABS_X
// - ABS_MT_POSITION_Y & ABS_Y
// - ABS_MT_PRESSURE & ABS_PRESSURE
// - ABS_MT_TOUCH_MAJOR
// - ABS_MT_TOUCH_MINOR
// - BTN_TOUCH
// Any other code, like MSC_TIMESTAMP, is not implemented.
type TouchscreenEventWriter struct {
rw *RawEventWriter
virt *os.File // if non-nil, used to hold a virtual device open
dev string // path to underlying device in /dev/input
nextTouchID int32
width TouchCoord
height TouchCoord
maxTouchSlot int
maxTrackingID int
maxPressure int
// clockwise rotation in degree to translate event location. It only supports
// 0, 90, 180, or 270 degrees.
rotation int
}
var nextVirtTouchNum = 1 // appended to virtual touchscreen device name
const touchFrequency = 5 * time.Millisecond
// Touchscreen returns an TouchscreenEventWriter to inject events into an arbitrary touchscreen device.
func Touchscreen(ctx context.Context) (*TouchscreenEventWriter, error) {
infos, err := readDevices("")
if err != nil {
return nil, errors.Wrap(err, "failed to read devices")
}
for _, info := range infos {
if !info.isTouchscreen() {
continue
}
testing.ContextLogf(ctx, "Opening touchscreen device %+v", info)
// Get touchscreen properties: bounds, max touches, max pressure and max track id.
f, err := os.Open(info.path)
if err != nil {
return nil, err
}
defer f.Close()
var infoX, infoY, infoSlot, infoTrackingID, infoPressure absInfo
for _, entry := range []struct {
ec EventCode
dst *absInfo
}{
{ABS_X, &infoX},
{ABS_Y, &infoY},
{ABS_MT_SLOT, &infoSlot},
{ABS_MT_TRACKING_ID, &infoTrackingID},
{ABS_MT_PRESSURE, &infoPressure},
} {
if err := ioctl(int(f.Fd()), evIOCGAbs(uint(entry.ec)), uintptr(unsafe.Pointer(entry.dst))); err != nil {
return nil, err
}
}
if infoTrackingID.maximum < infoSlot.maximum {
return nil, errors.Errorf("invalid MT tracking ID %d; should be >= max slots %d",
infoTrackingID.maximum, infoSlot.maximum)
}
if infoX.maximum == 0 || infoY.maximum == 0 {
return nil, errors.Errorf("invalid screen size (%d, %d)", infoX.maximum, infoY.maximum)
}
device, err := Device(ctx, info.path)
if err != nil {
return nil, err
}
return &TouchscreenEventWriter{
rw: device,
width: TouchCoord(infoX.maximum),
height: TouchCoord(infoY.maximum),
maxTouchSlot: int(infoSlot.maximum),
maxTrackingID: int(infoTrackingID.maximum),
maxPressure: int(infoPressure.maximum),
}, nil
}
// If we didn't find a real touchscreen, create a virtual one.
return VirtualTouchscreen(ctx)
}
// VirtualTouchscreen creates a virtual touchscreen device and returns an EventWriter that injects events into it.
func VirtualTouchscreen(ctx context.Context) (*TouchscreenEventWriter, error) {
const (
// Most touchscreens use I2C bus. But hardcoding to USB since it is supported
// in all Chromebook devices.
busType = 0x3 // BUS_USB from input.h
// Device constants taken from Chromebook Slate.
vendor = 0x2d1f
product = 0x5143
version = 0x100
// Input characteristics.
props = 1 << INPUT_PROP_DIRECT
evTypes = 1<<EV_KEY | 1<<EV_ABS | 1<<EV_MSC
// Abs axes supported in our virtual device.
absSupportedAxes = 1<<ABS_X | 1<<ABS_Y | 1<<ABS_PRESSURE | 1<<ABS_MT_SLOT |
1<<ABS_MT_TOUCH_MAJOR | 1<<ABS_MT_TOUCH_MINOR | 1<<ABS_MT_ORIENTATION |
1<<ABS_MT_POSITION_X | 1<<ABS_MT_POSITION_Y | 1<<ABS_MT_TOOL_TYPE |
1<<ABS_MT_TRACKING_ID | 1<<ABS_MT_PRESSURE
// Abs axis constants. Taken from Chromebook Slate.
axisMaxX = 10404
axisMaxY = 6936
axisMaxTracking = 65535
axisMaxPressure = 255
axisCoordResolution = 40
)
axisMaxTouchSlot := 9
// Include our PID in the device name to be extra careful in case an old bundle process hasn't exited.
name := fmt.Sprintf("Tast virtual touchscreen %d.%d", os.Getpid(), nextVirtTouchNum)
nextVirtTouchNum++
testing.ContextLogf(ctx, "Creating virtual touchscreen device %q", name)
dev, virt, err := createVirtual(name, devID{busType, vendor, product, version}, props, evTypes,
map[EventType]*big.Int{
EV_KEY: makeBigInt([]uint64{0x400, 0, 0, 0, 0, 0}), // BTN_TOUCH
EV_ABS: big.NewInt(absSupportedAxes),
EV_MSC: big.NewInt(1 << MSC_TIMESTAMP),
}, map[EventCode]Axis{
ABS_X: {axisMaxX, 0, 0, 0, axisCoordResolution},
ABS_Y: {axisMaxY, 0, 0, 0, axisCoordResolution},
ABS_PRESSURE: {axisMaxPressure, 0, 0, 0, 0},
ABS_MT_SLOT: {int32(axisMaxTouchSlot), 0, 0, 0, 0},
ABS_MT_TOUCH_MAJOR: {255, 0, 0, 0, 1},
ABS_MT_TOUCH_MINOR: {255, 0, 0, 0, 1},
ABS_MT_ORIENTATION: {1, 0, 0, 0, 0},
ABS_MT_POSITION_X: {axisMaxX, 0, 0, 0, axisCoordResolution},
ABS_MT_POSITION_Y: {axisMaxY, 0, 0, 0, axisCoordResolution},
ABS_MT_TOOL_TYPE: {2, 0, 0, 0, 0},
ABS_MT_TRACKING_ID: {axisMaxTracking, 0, 0, 0, 0},
ABS_MT_PRESSURE: {axisMaxPressure, 0, 0, 0, 0},
})
if err != nil {
return nil, err
}
// After initializing the virtual device a pause is needed to be able to detect the device.
// TODO(crbug.com/1015264): Remove the hard-coded sleep.
if err := testing.Sleep(ctx, 1*time.Second); err != nil {
return nil, err
}
device, err := Device(ctx, dev)
if err != nil {
return nil, err
}
return &TouchscreenEventWriter{
rw: device,
dev: dev,
virt: virt,
width: axisMaxX,
height: axisMaxY,
maxTouchSlot: axisMaxTouchSlot,
maxTrackingID: axisMaxTracking,
maxPressure: axisMaxPressure,
}, nil
}
// Close closes the touchscreen device.
func (tsw *TouchscreenEventWriter) Close() error {
firstErr := tsw.rw.Close()
// Let go the virtual device if any.
if tsw.virt != nil {
if err := tsw.virt.Close(); firstErr == nil {
firstErr = err
}
}
return firstErr
}
// NewMultiTouchWriter returns a new TouchEventWriter instance. numTouches is how many touches
// are going to be used by the TouchEventWriter.
func (tsw *TouchscreenEventWriter) NewMultiTouchWriter(numTouches int) (*TouchEventWriter, error) {
if numTouches < 1 || numTouches > tsw.maxTouchSlot {
return nil, errors.Errorf("requested %d touches; device only supports a max of %d touches", numTouches, tsw.maxTouchSlot+1)
}
tw := TouchEventWriter{tsw: tsw, touchStartTime: tsw.rw.nowFunc()}
tw.initTouchState(numTouches)
return &tw, nil
}
// NewSingleTouchWriter returns a new SingleTouchEventWriter instance.
// The difference between calling NewSingleTouchWriter() and NewMultiTouchWriter(1)
// is that NewSingleTouchWriter() has the extra helper Move() method.
func (tsw *TouchscreenEventWriter) NewSingleTouchWriter() (*SingleTouchEventWriter, error) {
stw := SingleTouchEventWriter{TouchEventWriter{tsw: tsw, touchStartTime: tsw.rw.nowFunc()}}
stw.initTouchState(1)
return &stw, nil
}
// Width returns the width of the touchscreen device, in touchscreen coordinates.
// This is affected by the rotation of the screen.
func (tsw *TouchscreenEventWriter) Width() TouchCoord {
if tsw.rotation == 90 || tsw.rotation == 270 {
return tsw.height
}
return tsw.width
}
// Height returns the height of the touchscreen device, in touchscreen coordinates.
// This is affected by the rotation of the screen.
func (tsw *TouchscreenEventWriter) Height() TouchCoord {
if tsw.rotation == 90 || tsw.rotation == 270 {
return tsw.width
}
return tsw.height
}
// SetRotation changes the orientation of the touch screen's event to the
// specified degree. The locations of further touch events will be rotated by
// the specified rotation. It will return an error if the specified rotation is
// not supported.
func (tsw *TouchscreenEventWriter) SetRotation(rotation int) error {
rotation = rotation % 360
if rotation < 0 {
rotation += 360
}
if rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270 {
return errors.Errorf("unsupported rotation: %d", rotation)
}
tsw.rotation = rotation
return nil
}
// TouchCoordConverter manages the conversion between locations in DIP and
// the TouchCoord of the touchscreen.
type TouchCoordConverter struct {
ScaleX float64
ScaleY float64
}
// NewTouchCoordConverter creates a new TouchCoordConverter instance for the
// given size.
func (tsw *TouchscreenEventWriter) NewTouchCoordConverter(size coords.Size) *TouchCoordConverter {
return &TouchCoordConverter{
ScaleX: float64(tsw.Width()) / float64(size.Width),
ScaleY: float64(tsw.Height()) / float64(size.Height),
}
}
// ConvertLocation converts a location to TouchCoord.
func (tcc *TouchCoordConverter) ConvertLocation(l coords.Point) (x, y TouchCoord) {
return TouchCoord(tcc.ScaleX * float64(l.X)), TouchCoord(tcc.ScaleY * float64(l.Y))
}
// TouchEventWriter supports injecting touch events into a touchscreen device.
type TouchEventWriter struct {
tsw *TouchscreenEventWriter
touches []TouchState
touchStartTime time.Time
ended bool
}
// SingleTouchEventWriter supports injecting a single touch into a touchscreen device.
type SingleTouchEventWriter struct {
TouchEventWriter
}
// TouchState contains the state of a single touch event.
type TouchState struct {
tsw *TouchscreenEventWriter
slot int32
touchID int32
touchMinor int32
touchMajor int32
absPressure int32
x TouchCoord
y TouchCoord
}
// SetPos sets TouchState X and Y coordinates.
// X and Y must be between [0, touchscreen width) and [0, touchscreen height).
func (ts *TouchState) SetPos(x, y TouchCoord) error {
if x < 0 || x >= ts.tsw.Width() || y < 0 || y >= ts.tsw.Height() {
return errors.Errorf("coordinates (%d, %d) outside valid bounds [0, %d), [0, %d)",
x, y, ts.tsw.Width(), ts.tsw.Height())
}
switch ts.tsw.rotation {
case 90:
x, y = ts.tsw.width-1-y, x
case 180:
x, y = ts.tsw.width-1-x, ts.tsw.height-1-y
case 270:
x, y = y, ts.tsw.height-1-x
}
ts.x = x
ts.y = y
return nil
}
// absInfo corresponds to a input_absinfo struct.
// Taken from: include/uapi/linux/input.h
type absInfo struct {
value uint32
minimum uint32
maximum uint32
fuzz uint32
flat uint32
resolution uint32
}
// evIOCGAbs returns an encoded Event-Ioctl-Get-Absolute value to be used for ioctl().
// Similar to the EVIOCGABS found in include/uapi/linux/input.h
func evIOCGAbs(ev uint) uint {
const sizeofAbsInfo = 0x24
return ior('E', 0x40+ev, sizeofAbsInfo)
}
// evIOCSAbs sets an encoded Event-Ioctl-Set-Absolute value to be used for ioctl().
// Similar to the EVIOCSABS found in include/uapi/linux/input.h
func evIOCSAbs(ev uint) uint {
const sizeofAbsInfo = 0x24
return iow('E', 0xc0+ev, sizeofAbsInfo)
}
type kernelEventEntry struct {
et EventType
ec EventCode
val int32
}
// Send sends all the multi-touch events to the kernel.
func (tw *TouchEventWriter) Send() error {
// First send the multitouch event codes.
for _, touch := range tw.touches {
for _, e := range []kernelEventEntry{
{EV_ABS, ABS_MT_SLOT, touch.slot},
{EV_ABS, ABS_MT_TRACKING_ID, touch.touchID},
{EV_ABS, ABS_MT_POSITION_X, int32(touch.x)},
{EV_ABS, ABS_MT_POSITION_Y, int32(touch.y)},
{EV_ABS, ABS_MT_PRESSURE, touch.absPressure},
{EV_ABS, ABS_MT_TOUCH_MAJOR, touch.touchMajor},
{EV_ABS, ABS_MT_TOUCH_MINOR, touch.touchMinor},
} {
if err := tw.tsw.rw.Event(e.et, e.ec, e.val); err != nil {
return err
}
}
}
// Then send the rest of the event codes.
for _, e := range []kernelEventEntry{
{EV_KEY, BTN_TOUCH, 1},
{EV_ABS, ABS_X, int32(tw.touches[0].x)},
{EV_ABS, ABS_Y, int32(tw.touches[0].y)},
{EV_ABS, ABS_PRESSURE, tw.touches[0].absPressure},
} {
if err := tw.tsw.rw.Event(e.et, e.ec, e.val); err != nil {
return err
}
}
tw.ended = false
// And finally sync.
return tw.tsw.rw.Sync()
}
// End injects a "touch lift" like if someone were lifting the finger or
// stylus from the surface. All active TouchStates are ended.
func (tw *TouchEventWriter) End() error {
for _, touch := range tw.touches {
for _, e := range []kernelEventEntry{
{EV_ABS, ABS_MT_SLOT, touch.slot},
{EV_ABS, ABS_MT_TRACKING_ID, -1},
} {
if err := tw.tsw.rw.Event(e.et, e.ec, e.val); err != nil {
return err
}
}
}
for _, e := range []kernelEventEntry{
{EV_ABS, ABS_PRESSURE, 0},
{EV_KEY, BTN_TOUCH, 0},
} {
if err := tw.tsw.rw.Event(e.et, e.ec, e.val); err != nil {
return err
}
}
tw.ended = true
return tw.tsw.rw.Sync()
}
// Close cleans up TouchEventWriter. This method must be called after using it,
// possibly with the "defer" statement.
func (tw *TouchEventWriter) Close() {
if !tw.ended {
tw.End()
}
}
// Swipe performs a swipe movement with an user defined number of touches. The touches are separated in the x
// coordinates by d. So for a 3 touch swipe, the initial touches will be (x0, y0), (x0+d, y0) and (x0+2d, y0).
// t represents how long the swipe should last. If t is less than 5 milliseconds, 5 milliseconds will be used instead.
// Swipe() does not call End(), allowing the user to concatenate multiple swipes together.
func (tw *TouchEventWriter) Swipe(ctx context.Context, x0, y0, x1, y1, d TouchCoord, touches int, t time.Duration) error {
if len(tw.touches) < touches {
return errors.Errorf("requested %d touches for swipe; got %d", touches, len(tw.touches))
}
steps := int(t/touchFrequency) + 1
// A minimum of two touches are needed. One for the start point and another one for the end point.
if steps < 2 {
steps = 2
}
deltaX := float64(x1-x0) / float64(steps-1)
deltaY := float64(y1-y0) / float64(steps-1)
for i := 0; i < steps; i++ {
x := x0 + TouchCoord(math.Round(deltaX*float64(i)))
y := y0 + TouchCoord(math.Round(deltaY*float64(i)))
for j := 0; j < touches; j++ {
if err := tw.touches[j].SetPos(x+TouchCoord(j)*d, y); err != nil {
return err
}
}
if err := tw.Send(); err != nil {
return err
}
if err := testing.Sleep(ctx, touchFrequency); err != nil {
return errors.Wrap(err, "timeout while doing sleep")
}
}
return nil
}
// DoubleSwipe performs a swipe movement with two touches. One is from x0/y0 to x1/y1, and the other is x0+d/y0 to x1+d/y1.
// t represents how long the swipe should last.
// If t is less than 5 milliseconds, 5 milliseconds will be used instead.
// DoubleSwipe() does not call End(), allowing the user to concatenate multiple swipes together.
func (tw *TouchEventWriter) DoubleSwipe(ctx context.Context, x0, y0, x1, y1, d TouchCoord, t time.Duration) error {
return tw.Swipe(ctx, x0, y0, x1, y1, d, 2, t)
}
// Move injects a touch event at x and y touchscreen coordinates. This is applied
// only to the first TouchState. Calling this function is equivalent to:
// ts := touchEventWriter.TouchState(0)
// ts.SetPos(x, y)
// ts.Send()
func (stw *SingleTouchEventWriter) Move(x, y TouchCoord) error {
if err := stw.touches[0].SetPos(x, y); err != nil {
return err
}
return stw.Send()
}
// LongPressAt injects a touch event at (x, y) touchscreen coordinates and wait
// a bit to simulate a touch long press. The wait time should be longer than
// chrome's default long press wait time, which is 500ms.
// See ui/events/gesture_detection/gesture_detector.cc in chromium.
func (stw *SingleTouchEventWriter) LongPressAt(ctx context.Context, x, y TouchCoord) error {
if err := stw.Move(x, y); err != nil {
return err
}
return testing.Sleep(ctx, 1*time.Second)
}
// Swipe performs a swipe movement from x0/y0 to x1/y1.
// t represents how long the swipe should last.
// If t is less than 5 milliseconds, 5 milliseconds will be used instead.
// Swipe() does not call End(), allowing the user to concatenate multiple swipes together.
func (stw *SingleTouchEventWriter) Swipe(ctx context.Context, x0, y0, x1, y1 TouchCoord, t time.Duration) error {
steps := int(t/touchFrequency) + 1
// A minimum of two touches are needed. One for the start point and another one for the end point.
if steps < 2 {
steps = 2
}
deltaX := float64(x1-x0) / float64(steps-1)
deltaY := float64(y1-y0) / float64(steps-1)
for i := 0; i < steps; i++ {
x := x0 + TouchCoord(math.Round(deltaX*float64(i)))
y := y0 + TouchCoord(math.Round(deltaY*float64(i)))
if err := stw.Move(x, y); err != nil {
return err
}
if err := testing.Sleep(ctx, touchFrequency); err != nil {
return errors.Wrap(err, "timeout while doing sleep")
}
}
return nil
}
// TouchState returns a TouchState. touchIndex is touch to get.
// One TouchState represents the state of a single touch.
func (tw *TouchEventWriter) TouchState(touchIndex int) *TouchState {
return &tw.touches[touchIndex]
}
func (tw *TouchEventWriter) initTouchState(numTouches int) {
// Values taken from "dumps" on an Eve device.
// Spec says pressure is in arbitrary units. A value around 25% of the max value seems to be "normal".
// TouchMajor and TouchMinor were also taken from "dumps".
const (
defaultTouchMajor = 5
defaultTouchMinor = 5
)
defaultPressure := int32(tw.tsw.maxPressure/4) + 1
tw.touches = make([]TouchState, numTouches)
for i := 0; i < numTouches; i++ {
tw.touches[i].tsw = tw.tsw
tw.touches[i].absPressure = defaultPressure
tw.touches[i].touchMajor = defaultTouchMajor
tw.touches[i].touchMinor = defaultTouchMinor
tw.touches[i].touchID = tw.tsw.nextTouchID
tw.touches[i].slot = int32(i)
tw.tsw.nextTouchID = (tw.tsw.nextTouchID + 1) % int32(tw.tsw.maxTrackingID)
}
}