blob: cef92a410bdf04f4cc3e87fa201ac51811a6f82e [file] [log] [blame]
# Copyright 2015 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.
import logging
import os
import subprocess
import tempfile
import time
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
class InputPlayback(object):
"""
Provides an interface for playback and emulating peripherals via evemu-*.
Example use: player = InputPlayback()
player.emulate(property_file=path_to_file)
player.find_connected_inputs()
player.playback(path_to_file)
player.blocking_playback(path_to_file)
player.close()
"""
_DEFAULT_PROPERTY_FILES = {'mouse' : 'mouse.prop',
'keyboard' : 'keyboard.prop'}
_PLAYBACK_COMMAND = 'evemu-play --insert-slot0 %s < %s'
# Define a keyboard as anything with any keys #2 to #248 inclusive,
# as defined in the linux input header. This definition includes things
# like the power button, so reserve the "keyboard" label for things with
# letters/numbers and define the rest as "other_keyboard".
_MINIMAL_KEYBOARD_KEYS = ['1', 'Q', 'SPACE']
_KEYBOARD_KEYS = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'MINUS', 'EQUAL',
'BACKSPACE', 'TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O',
'P', 'LEFTBRACE', 'RIGHTBRACE', 'ENTER', 'LEFTCTRL', 'A', 'S', 'D',
'F', 'G', 'H', 'J', 'K', 'L', 'SEMICOLON', 'APOSTROPHE', 'GRAVE',
'LEFTSHIFT', 'BACKSLASH', 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
'COMMA', 'DOT', 'SLASH', 'RIGHTSHIFT', 'KPASTERISK', 'LEFTALT',
'SPACE', 'CAPSLOCK', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8',
'F9', 'F10', 'NUMLOCK', 'SCROLLLOCK', 'KP7', 'KP8', 'KP9',
'KPMINUS', 'KP4', 'KP5', 'KP6', 'KPPLUS', 'KP1', 'KP2', 'KP3',
'KP0', 'KPDOT', 'ZENKAKUHANKAKU', '102ND', 'F11', 'F12', 'RO',
'KATAKANA', 'HIRAGANA', 'HENKAN', 'KATAKANAHIRAGANA', 'MUHENKAN',
'KPJPCOMMA', 'KPENTER', 'RIGHTCTRL', 'KPSLASH', 'SYSRQ', 'RIGHTALT',
'LINEFEED', 'HOME', 'UP', 'PAGEUP', 'LEFT', 'RIGHT', 'END', 'DOWN',
'PAGEDOWN', 'INSERT', 'DELETE', 'MACRO', 'MUTE', 'VOLUMEDOWN',
'VOLUMEUP', 'POWER', 'KPEQUAL', 'KPPLUSMINUS', 'PAUSE', 'SCALE',
'KPCOMMA', 'HANGEUL', 'HANGUEL', 'HANJA', 'YEN', 'LEFTMETA',
'RIGHTMETA', 'COMPOSE', 'STOP', 'AGAIN', 'PROPS', 'UNDO', 'FRONT',
'COPY', 'OPEN', 'PASTE', 'FIND', 'CUT', 'HELP', 'MENU', 'CALC',
'SETUP', 'WAKEUP', 'FILE', 'SENDFILE', 'DELETEFILE', 'XFER',
'PROG1', 'PROG2', 'WWW', 'MSDOS', 'COFFEE', 'SCREENLOCK',
'DIRECTION', 'CYCLEWINDOWS', 'MAIL', 'BOOKMARKS', 'COMPUTER',
'BACK', 'FORWARD', 'CLOSECD', 'EJECTCD', 'EJECTCLOSECD', 'NEXTSONG',
'PLAYPAUSE', 'PREVIOUSSONG', 'STOPCD', 'RECORD', 'REWIND', 'PHONE',
'ISO', 'CONFIG', 'HOMEPAGE', 'REFRESH', 'EXIT', 'MOVE', 'EDIT',
'SCROLLUP', 'SCROLLDOWN', 'KPLEFTPAREN', 'KPRIGHTPAREN', 'NEW',
'REDO', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
'F21', 'F22', 'F23', 'F24', 'PLAYCD', 'PAUSECD', 'PROG3', 'PROG4',
'DASHBOARD', 'SUSPEND', 'CLOSE', 'PLAY', 'FASTFORWARD', 'BASSBOOST',
'PRINT', 'HP', 'CAMERA', 'SOUND', 'QUESTION', 'EMAIL', 'CHAT',
'SEARCH', 'CONNECT', 'FINANCE', 'SPORT', 'SHOP', 'ALTERASE',
'CANCEL', 'BRIGHTNESSDOWN', 'BRIGHTNESSUP', 'MEDIA',
'SWITCHVIDEOMODE', 'KBDILLUMTOGGLE', 'KBDILLUMDOWN', 'KBDILLUMUP',
'SEND', 'REPLY', 'FORWARDMAIL', 'SAVE', 'DOCUMENTS', 'BATTERY',
'BLUETOOTH', 'WLAN', 'UWB', 'UNKNOWN', 'VIDEO_NEXT', 'VIDEO_PREV',
'BRIGHTNESS_CYCLE', 'BRIGHTNESS_AUTO', 'BRIGHTNESS_ZERO',
'DISPLAY_OFF', 'WWAN', 'WIMAX', 'RFKILL', 'MICMUTE']
def __init__(self):
self.nodes = {} # e.g. /dev/input/event4
self.device_dirs = {} # e.g. /sys/class/event4/device/device
self.names = {} # e.g. Atmel maXTouch Touchpad
self._device_emulation_process = None
def has(self, input_type):
"""Return True/False if device has a input of given type.
@param input_type: string of type, e.g. 'touchpad'
"""
return input_type in self.nodes
def _get_input_events(self):
"""Return a list of all input event nodes."""
return utils.run('ls /dev/input/event*').stdout.strip().split()
def emulate(self, input_type='mouse', property_file=None):
"""
Emulate the given input (or default for type) with evemu-device.
Emulating more than one of the same device type will only allow playback
on the last one emulated. The name of the last-emulated device is
noted to be sure this is the case.
Property files are made with the evemu-describe command,
e.g. 'evemu-describe /dev/input/event12 > property_file'.
@param input_type: 'mouse' or 'keyboard' to use default property files.
Need not be specified if supplying own file.
@param property_file: Property file of device to be emulated. Generate
with 'evemu-describe' command on test image.
"""
# Checks for any previous emulated device and kills the process
if self._device_emulation_process:
self.close()
if not property_file:
if input_type not in self._DEFAULT_PROPERTY_FILES:
raise error.TestError('Please supply a property file for input '
'type %s' % input_type)
current_dir = os.path.dirname(os.path.realpath(__file__))
property_file = os.path.join(
current_dir, self._DEFAULT_PROPERTY_FILES[input_type])
if not os.path.isfile(property_file):
raise error.TestError('Property file %s not found!' % property_file)
logging.info('Emulating %s %s', input_type, property_file)
num_events_before = len(self._get_input_events())
self._device_emulation_process = subprocess.Popen(
['evemu-device', property_file], stdout=subprocess.PIPE)
utils.poll_for_condition(
lambda: len(self._get_input_events()) > num_events_before,
exception=error.TestError('Error emulating %s!' % input_type))
with open(property_file) as fh:
name_line = fh.readline() #Format "N: NAMEOFDEVICE"
self.names[input_type] = name_line[3:-1]
def _find_device_properties(self, device):
"""Return string of properties for given node.
@return: string of properties.
"""
with tempfile.NamedTemporaryFile() as temp_file:
filename = temp_file.name
evtest_process = subprocess.Popen(['evtest', device],
stdout=temp_file)
def find_exit():
"""Polling function for end of output."""
interrupt_cmd = 'grep "interrupt to exit" %s | wc -l' % filename
line_count = utils.run(interrupt_cmd).stdout.strip()
return line_count != '0'
utils.poll_for_condition(find_exit)
evtest_process.kill()
temp_file.seek(0)
props = temp_file.read()
return props
def _determine_input_type(self, props):
"""Find input type (if any) from a string of properties.
@return: string of type, or None
"""
if props.find('REL_X') >= 0 and props.find('REL_Y') >= 0:
if (props.find('ABS_MT_POSITION_X') >= 0 and
props.find('ABS_MT_POSITION_Y') >= 0):
return 'multitouch_mouse'
else:
return 'mouse'
if props.find('ABS_X') >= 0 and props.find('ABS_Y') >= 0:
if (props.find('BTN_STYLUS') >= 0 or
props.find('BTN_STYLUS2') >= 0 or
props.find('BTN_TOOL_PEN') >= 0):
return 'tablet'
if (props.find('ABS_PRESSURE') >= 0 or
props.find('BTN_TOUCH') >= 0):
if (props.find('BTN_LEFT') >= 0 or
props.find('BTN_MIDDLE') >= 0 or
props.find('BTN_RIGHT') >= 0 or
props.find('BTN_TOOL_FINGER') >= 0):
return 'touchpad'
else:
return 'touchscreen'
if props.find('BTN_LEFT') >= 0:
return 'touchscreen'
if props.find('KEY_') >= 0:
for key in self._MINIMAL_KEYBOARD_KEYS:
if props.find('KEY_%s' % key) >= 0:
return 'keyboard'
for key in self._KEYBOARD_KEYS:
if props.find('KEY_%s' % key) >= 0:
return 'other_keyboard'
return
def find_connected_inputs(self):
"""Determine the nodes of all present input devices, if any.
Cycle through all possible /dev/input/event* and find which ones
are touchpads, touchscreens, mice, keyboards, etc.
These nodes can be used for playback later.
If a name already exists in self.names, prefer that device.
Record the found nodes in self.nodes and their names in self.names.
"""
self.nodes = {} #Discard any previously seen nodes.
input_events = self._get_input_events()
for event in input_events:
properties = self._find_device_properties(event)
input_type = self._determine_input_type(properties)
if input_type:
class_folder = event.replace('dev', 'sys/class')
name_file = os.path.join(class_folder, 'device', 'name')
name = 'unknown'
if os.path.isfile(name_file):
name = utils.run('cat %s' % name_file).stdout.strip()
logging.info('Found %s: %s at %s.', input_type, name, event)
# If a particular device is expected, make sure name matches.
if input_type in self.names:
if self.names[input_type] != name:
continue
# Find the devices folder containing power info
# e.g. /sys/class/event4/device/device
device_dir = os.path.join(class_folder, 'device', 'device')
if not os.path.exists(device_dir):
device_dir = None
# Save this device information for later use.
self.nodes[input_type] = event
self.names[input_type] = name
self.device_dirs[input_type] = device_dir
def playback(self, filepath, input_type='touchpad'):
"""Playback a given input file.
Create input file using evemu-record.
E.g. 'evemu-record $NODE -1 > $FILENAME'
@param filepath: path to the input file on the DUT.
@param input_type: name of device type; 'touchpad' by default.
Types are returned by the _determine_input_type()
function.
input_type must be in self.nodes. Check using has().
"""
assert(input_type in self.nodes)
node = self.nodes[input_type]
logging.info('Playing back finger-movement on %s, file=%s.', node,
filepath)
utils.run(self._PLAYBACK_COMMAND % (node, filepath))
def blocking_playback(self, filepath, input_type='touchpad'):
"""Playback a given set of inputs and sleep for duration.
The input file is of the format <name>\nE: <time> <input>\nE: ...
Find the total time by the difference between the first and last input.
@param filepath: path to the input file on the DUT.
@param input_type: name of device type; 'touchpad' by default.
Types are returned by the _determine_input_type()
function.
input_type must be in self.nodes. Check using has().
"""
with open(filepath) as fh:
lines = fh.readlines()
start = float(lines[0].split(' ')[1])
end = float(lines[-1].split(' ')[1])
sleep_time = end - start
self.playback(filepath, input_type)
logging.info('Sleeping for %s seconds during playback.', sleep_time)
time.sleep(sleep_time)
def blocking_playback_of_default_file(self, filename, input_type='mouse'):
"""Playback a default file and sleep for duration.
Use a default gesture file for the default keyboard/mouse, saved in
this folder.
Device should be emulated first.
@param filename: the name of the file (path is to this folder).
@param input_type: name of device type; 'mouse' by default.
Types are returned by the _determine_input_type()
function.
input_type must be in self.nodes. Check using has().
"""
current_dir = os.path.dirname(os.path.realpath(__file__))
gesture_file = os.path.join(current_dir, filename)
self.blocking_playback(gesture_file, input_type=input_type)
def close(self):
"""Kill emulation if necessary."""
if self._device_emulation_process:
self._device_emulation_process.kill()
def __exit__(self):
self.close()