1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3
# Autopilot Functional Test Tool
4
# Copyright (C) 2012-2013 Canonical
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
"""UInput device drivers."""
23
from autopilot.input import Keyboard as KeyboardBase
24
from autopilot.input import Touch as TouchBase
25
from autopilot.input._common import get_center_point
26
import autopilot.platform
29
from time import sleep
30
from evdev import UInput, ecodes as e
33
logger = logging.getLogger(__name__)
41
class Keyboard(KeyboardBase):
44
super(Keyboard, self).__init__()
46
self._device = UInput(devnode=_get_devnode_path())
48
def _emit(self, event, value):
49
self._device.write(e.EV_KEY, event, value)
52
def _sanitise_keys(self, keys):
56
return keys.split('+')
58
def press(self, keys, delay=0.1):
59
"""Send key press events only.
61
The 'keys' argument must be a string of keys you want
66
presses the 'Alt' and 'F2' keys.
69
if not isinstance(keys, basestring):
70
raise TypeError("'keys' argument must be a string.")
72
for key in self._sanitise_keys(keys):
73
for event in Keyboard._get_events_for_key(key):
74
logger.debug("Pressing %s (%r)", key, event)
75
self._emit(event, PRESS)
78
def release(self, keys, delay=0.1):
79
"""Send key release events only.
81
The 'keys' argument must be a string of keys you want
82
released. For example:
86
releases the 'Alt' and 'F2' keys.
88
Keys are released in the reverse order in which they are specified.
91
if not isinstance(keys, basestring):
92
raise TypeError("'keys' argument must be a string.")
93
# logger.debug("Releasing keys %r with delay %f", keys, delay)
94
# # release keys in the reverse order they were pressed in.
95
# keys = self.__translate_keys(keys)
96
for key in reversed(self._sanitise_keys(keys)):
97
for event in Keyboard._get_events_for_key(key):
98
logger.debug("Releasing %s (%r)", key, event)
99
self._emit(event, RELEASE)
102
def press_and_release(self, keys, delay=0.1):
103
"""Press and release all items in 'keys'.
105
This is the same as calling 'press(keys);release(keys)'.
107
The 'keys' argument must be a string of keys you want
108
pressed and released.. For example:
110
press_and_release('Alt+F2')
112
presses both the 'Alt' and 'F2' keys, and then releases both keys.
115
logger.debug("Pressing and Releasing: %s", keys)
116
self.press(keys, delay)
117
self.release(keys, delay)
119
def type(self, string, delay=0.1):
120
"""Simulate a user typing a string of text.
122
Only 'normal' keys can be typed with this method. Control characters
123
(such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
126
if not isinstance(string, basestring):
127
raise TypeError("'keys' argument must be a string.")
128
logger.debug("Typing text %r", string)
130
self.press(key, delay)
131
self.release(key, delay)
135
"""Generate KeyRelease events for any un-released keys.
137
Make sure you call this at the end of any test to release
138
any keys that were pressed and not released.
141
# global _PRESSED_KEYS
142
# for keycode in _PRESSED_KEYS:
143
# logger.warning("Releasing key %r as part of cleanup call.", keycode)
144
# fake_input(get_display(), X.KeyRelease, keycode)
148
def _get_events_for_key(key):
149
"""Return a list of events required to generate 'key' as an input.
151
Multiple keys will be returned when the key specified requires more than one
152
keypress to generate (for example, upper-case letters).
156
if key.isupper() or key in _SHIFTED_KEYS:
157
events.append(e.KEY_LEFTSHIFT)
158
keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
159
evt = getattr(e, 'KEY_' + keyname.upper(), None)
161
raise ValueError("Unknown key name: '%s'" % key)
166
def _get_devnode_path():
167
"""Provide a fallback uinput node for devices which don't support udev"""
168
devnode = '/dev/autopilot-uinput'
169
if not os.path.exists(devnode):
170
devnode = '/dev/uinput'
175
def get_next_tracking_id():
176
global last_tracking_id
177
last_tracking_id += 1
178
return last_tracking_id
181
def create_touch_device(res_x=None, res_y=None):
182
"""Create and return a UInput touch device.
184
If res_x and res_y are not specified, they will be queried from the system.
188
if res_x is None or res_y is None:
189
from autopilot.display import Display
190
display = Display.create()
191
# TODO: This calculation needs to become part of the display module:
193
for screen in range(display.get_num_screens()):
194
geometry = display.get_screen_geometry(screen)
199
if geometry[0] + geometry[2] > r:
200
r = geometry[0] + geometry[2]
201
if geometry[1] + geometry[3] > b:
202
b = geometry[1] + geometry[3];
206
# android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
208
touch_tool = e.BTN_TOOL_FINGER
209
if autopilot.platform.model() == 'Desktop':
210
touch_tool = e.BTN_TOUCH
214
(e.ABS_X, (0, res_x, 0, 0)),
215
(e.ABS_Y, (0, res_y, 0, 0)),
216
(e.ABS_PRESSURE, (0, 65535, 0, 0)),
217
(e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
218
(e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
219
(e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
220
(e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
221
(e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
222
(e.ABS_MT_SLOT, (0, 9, 0, 0)),
229
return UInput(cap_mt, name='autopilot-finger', version=0x2,
230
devnode=_get_devnode_path())
232
_touch_device = create_touch_device()
237
# We're simulating a class of device that can track multiple touches, and keep
238
# them separate. This is how most modern track devices work anyway. The device
239
# is created with a capability to track a certain number of distinct touches at
240
# once. This is the ABS_MT_SLOT capability. Since our target device can track 9
241
# separate touches, we'll do the same.
243
# Each finger contact starts by registering a slot number (0-8) with a tracking
244
# Id. The Id should be unique for this touch - this can be an auto-inctrementing
245
# integer. The very first packets to tell the kernel that we have a touch happening
246
# should look like this:
249
# ABS_MT_TRACKING_ID 45
250
# ABS_MT_POSITION_X x[0]
251
# ABS_MT_POSITION_Y y[0]
253
# This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now
254
# not be use by any other touch until it is released.
256
# If we want to move this contact's coordinates, we do this:
259
# ABS_MT_POSITION_X 123
260
# ABS_MT_POSITION_Y 234
262
# Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but
263
# it doesn't hurt to have it there.
265
# To lift the contact, we simply specify a tracking Id of -1:
268
# ABS_MT_TRACKING_ID -1
270
# The initial association between slot and tracking Id is made when the 'finger'
271
# first makes contact with the device (well, not technically true, but close
272
# enough). Multiple touches can be active simultaniously, as long as they all have
273
# unique slots, and tracking Ids. The simplest way to think about this is that the
274
# SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch
275
# for the duration of it's existance.
277
_touch_fingers_in_use = []
278
def _get_touch_finger():
279
"""Claim a touch finger id for use.
281
:raises: RuntimeError if no more fingers are available.
284
global _touch_fingers_in_use
287
if i not in _touch_fingers_in_use:
288
_touch_fingers_in_use.append(i)
290
raise RuntimeError("All available fingers have been used already.")
292
def _release_touch_finger(finger_num):
293
"""Relase a previously-claimed finger id.
295
:raises: RuntimeError if the finger given was never claimed, or was already
299
global _touch_fingers_in_use
301
if finger_num not in _touch_fingers_in_use:
302
raise RuntimeError("Finger %d was never claimed, or has already been released." % (finger_num))
303
_touch_fingers_in_use.remove(finger_num)
304
assert(finger_num not in _touch_fingers_in_use)
307
class Touch(TouchBase):
308
"""Low level interface to generate single finger touch events."""
311
super(Touch, self).__init__()
312
self._touch_finger = None
316
return self._touch_finger is not None
319
"""Click (or 'tap') at given x and y coordinates."""
320
logger.debug("Tapping at: %d,%d", x,y)
321
self._finger_down(x, y)
325
def tap_object(self, object):
326
"""Click (or 'tap') a given object"""
327
logger.debug("Tapping object: %r", object)
328
x,y = get_center_point(object)
331
def press(self, x, y):
332
"""Press and hold a given object or at the given coordinates
333
Call release() when the object has been pressed long enough"""
334
logger.debug("Pressing at: %d,%d", x,y)
335
self._finger_down(x, y)
338
"""Release a previously pressed finger"""
339
logger.debug("Releasing")
343
def drag(self, x1, y1, x2, y2):
344
"""Perform a drag gesture from (x1,y1) to (x2,y2)"""
345
logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
346
self._finger_down(x1, y1)
348
# Let's drag in 100 steps for now...
349
dx = 1.0 * (x2 - x1) / 100
350
dy = 1.0 * (y2 - y1) / 100
353
for i in range(0, 100):
354
self._finger_move(int(cur_x), int(cur_y))
358
# Make sure we actually end up at target
359
self._finger_move(x2, y2)
364
def _finger_down(self, x, y):
365
"""Internal: moves finger "finger" down to the touchscreen at pos (x,y)"""
366
if self._touch_finger is not None:
367
raise RuntimeError("Cannot press finger: it's already pressed.")
368
self._touch_finger = _get_touch_finger()
370
_touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
371
_touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
372
_touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
373
_touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
374
_touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
375
_touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
379
def _finger_move(self, x, y):
380
"""Internal: moves finger "finger" on the touchscreen to pos (x,y)
381
NOTE: The finger has to be down for this to have any effect."""
382
if self._touch_finger is not None:
383
_touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
384
_touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
385
_touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
389
def _finger_up(self):
390
"""Internal: moves finger "finger" up from the touchscreen"""
391
if self._touch_finger is None:
392
raise RuntimeError("Cannot release finger: it's not pressed.")
393
_touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
394
_touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
395
_touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
397
self._touch_finger = _release_touch_finger(self._touch_finger)
400
# veebers: there should be a better way to handle this.
401
_SHIFTED_KEYS = "~!@#$%^&*()_+{}|:\"?><"
403
# The double-ups are due to the 'shifted' keys.
404
_UINPUT_CODE_TRANSLATIONS = {
441
'SHIFT': 'LEFTSHIFT',