blob: b7548b359898da4961e939b58bf9a812f78fe863 [file] [log] [blame]
# -*- coding: utf-8 -*-
'''
Copyright (C) 2012-2015 Diego Torres Milano
Created on oct 6, 2014
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.
@author: Diego Torres Milano
'''
import StringIO
import random
import time
import re
from com.dtmilano.android.common import profileStart
from com.dtmilano.android.common import profileEnd
from com.dtmilano.android.concertina import Concertina
__version__ = '11.5.9'
import sys
import threading
import warnings
import copy
import string
import os
import platform
from pkg_resources import Requirement, resource_filename
try:
import PIL
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except:
PIL_AVAILABLE = False
try:
import Tkinter
import tkSimpleDialog
import tkFileDialog
import tkFont
import ScrolledText
import ttk
from Tkconstants import DISABLED, NORMAL
TKINTER_AVAILABLE = True
except:
TKINTER_AVAILABLE = False
from ast import literal_eval as make_tuple
CHECK_KEYBOARD_SHOWN = False
PROFILE = False
DEBUG = False
DEBUG_MOVE = DEBUG and False
DEBUG_TOUCH = DEBUG and False
DEBUG_POINT = DEBUG and False
DEBUG_KEY = DEBUG and False
DEBUG_ISCCOF = DEBUG and False
DEBUG_FIND_VIEW = DEBUG and False
DEBUG_CONTEXT_MENU = DEBUG and False
DEBUG_CONCERTINA = DEBUG and False
DEBUG_UI_AUTOMATOR_HELPER = DEBUG and False
class Color:
GOLD = '#d19615'
GREEN = '#15d137'
BLUE = '#1551d1'
MAGENTA = '#d115af'
DARK_GRAY = '#222222'
LIGHT_GRAY = '#dddddd'
class Unit:
PX = 'PX'
DIP = 'DIP'
class Operation:
ASSIGN = 'assign'
CHANGE_LANGUAGE = 'change_language'
DEFAULT = 'default'
DRAG = 'drag'
DUMP = 'dump'
FLING_BACKWARD = 'fling_backward'
FLING_FORWARD = 'fling_forward'
FLING_TO_BEGINNING = 'fling_to_beginning'
FLING_TO_END = 'fling_to_end'
TEST = 'test'
TEST_TEXT = 'test_text'
TOUCH_VIEW = 'touch_view'
TOUCH_VIEW_UI_AUTOMATOR_HELPER = 'touch_view_ui_automator_helper'
TOUCH_POINT = 'touch_point'
LONG_TOUCH_POINT = 'long_touch_point'
LONG_TOUCH_VIEW = 'long_touch_view'
LONG_TOUCH_VIEW_UI_AUTOMATOR_HELPER = 'long_touch_view_ui_automator_helper'
OPEN_NOTIFICATION = 'open_notification'
OPEN_QUICK_SETTINGS = 'open_quick_settings'
TYPE = 'type'
PRESS = 'press'
PRESS_BACK = 'press_back'
PRESS_BACK_UI_AUTOMATOR_HELPER = 'press_back_ui_automator_helper'
PRESS_HOME = 'press_home'
PRESS_HOME_UI_AUTOMATOR_HELPER = 'press_home_ui_automator_helper'
PRESS_RECENT_APPS = 'press_recent_apps'
PRESS_RECENT_APPS_UI_AUTOMATOR_HELPER = 'press_recent_apps_ui_automator_helper'
SET_TEXT = 'set_text'
SNAPSHOT = 'snapshot'
START_ACTIVITY = 'start_activity'
SLEEP = 'sleep'
SWIPE_UI_AUTOMATOR_HELPER = 'swipe_ui_automator_helper'
TRAVERSE = 'traverse'
VIEW_SNAPSHOT = 'view_snapshot'
WAKE = 'wake'
COMMAND_NAME_OPERATION_MAP = {'flingBackward': FLING_BACKWARD, 'flingForward': FLING_FORWARD,
'flingToBeginning': FLING_TO_BEGINNING, 'flingToEnd': FLING_TO_END,
'openNotification': OPEN_NOTIFICATION, 'openQuickSettings': OPEN_QUICK_SETTINGS,
}
@staticmethod
def fromCommandName(commandName):
return Operation.COMMAND_NAME_OPERATION_MAP[commandName]
@staticmethod
def toCommandName(operation):
return next((cmd for cmd, op in Operation.COMMAND_NAME_OPERATION_MAP.items() if op == operation), None)
class Culebron:
APPLICATION_NAME = "Culebra"
UPPERCASE_CHARS = string.uppercase[:26]
KEYSYM_TO_KEYCODE_MAP = {
'Home': 'HOME',
'BackSpace': 'BACK',
'Left': 'DPAD_LEFT',
'Right': 'DPAD_RIGHT',
'Up': 'DPAD_UP',
'Down': 'DPAD_DOWN',
}
KEYSYM_CULEBRON_COMMANDS = {
'F1': None,
'F5': None
}
canvas = None
imageId = None
vignetteId = None
areTargetsMarked = False
isDragDialogShowed = False
isGrabbingTouch = False
isGeneratingTestCondition = False
isTouchingPoint = False
isLongTouchingPoint = False
isLongTouchingView = False
onTouchListener = None
snapshotDir = '/tmp'
snapshotFormat = 'PNG'
deviceArt = None
dropShadow = False
screenGlare = False
osName = platform.system()
''' The OS name. We sometimes need specific behavior. '''
isDarwin = (osName == 'Darwin')
''' Is it Mac OSX? '''
@staticmethod
def checkSupportedSdkVersion(sdkVersion):
if sdkVersion <= 10:
raise Exception('''culebra GUI requires Android API > 10 to work''')
@staticmethod
def checkDependencies():
if not PIL_AVAILABLE:
raise Exception('''PIL or Pillow is needed for GUI mode
On Ubuntu install
$ sudo apt-get install python-imaging python-imaging-tk
On OSX install
$ brew install homebrew/python/pillow
or, preferred since El Capitan
$ sudo easy_install pip
$ sudo pip install pillow
''')
if not TKINTER_AVAILABLE:
raise Exception('''Tkinter is needed for GUI mode
This is usually installed by python package. Check your distribution details.
''')
def __init__(self, vc, device, serialno, printOperation, scale=1, concertina=False):
'''
Culebron constructor.
@param vc: The ViewClient used by this Culebron instance. Can be C{None} if no back-end is used.
@type vc: ViewClient
@param device: The device
@type device: L{AdbClient}
@param serialno: The device's serial number
@type serialno: str
@param printOperation: the method invoked to print operations to the script
@type printOperation: method
@param scale: the scale of the device screen used to show it on the window
@type scale: float
@:param concertina: bool
@:type concertina: enable concertina mode (see documentation)
'''
self.vc = vc
self.printOperation = printOperation
self.device = device
self.sdkVersion = device.getSdkVersion()
self.serialno = serialno
self.scale = scale
self.concertina = concertina
self.window = Tkinter.Tk()
try:
f = resource_filename(Requirement.parse("androidviewclient"),
"share/pixmaps/culebra.png")
icon = ImageTk.PhotoImage(file=f)
except:
icon = None
if icon:
self.window.tk.call('wm', 'iconphoto', self.window._w, icon)
self.mainMenu = MainMenu(self)
self.window.config(menu=self.mainMenu)
self.mainFrame = Tkinter.Frame(self.window)
self.placeholder = Tkinter.Frame(self.mainFrame, width=400, height=400, background=Color.LIGHT_GRAY)
self.placeholder.grid(row=1, column=1, rowspan=4)
self.sideFrame = Tkinter.Frame(self.window)
self.viewTree = ViewTree(self.sideFrame)
self.viewDetails = ViewDetails(self.sideFrame)
self.mainFrame.grid(row=1, column=1, columnspan=1, rowspan=4, sticky=Tkinter.N + Tkinter.S)
self.isSideFrameShown = False
self.isViewTreeShown = False
self.isViewDetailsShown = False
self.statusBar = StatusBar(self.window)
self.statusBar.grid(row=5, column=1, columnspan=2)
self.statusBar.set("Always press F1 for help")
self.window.update_idletasks()
self.markedTargetIds = {}
self.isTouchingPoint = self.vc is None
self.coordinatesUnit = Unit.DIP
self.isLongTouchingPoint = False
self.isLongTouchingView = False
self.permanentlyDisableEvents = False
self.unscaledScreenshot = None
self.image = None
self.screenshot = None
if DEBUG:
try:
self.printGridInfo()
except:
pass
def printGridInfo(self):
print >> sys.stderr, "window:", repr(self.window)
print >> sys.stderr, "main:", repr(self.mainFrame)
print >> sys.stderr, "main:", self.mainFrame.grid_info()
print >> sys.stderr, "side:", repr(self.sideFrame)
print >> sys.stderr, "side:", self.sideFrame.grid_info()
print >> sys.stderr, "tree:", repr(self.viewTree)
print >> sys.stderr, "tree:", self.viewTree.grid_info()
print >> sys.stderr, "details:", repr(self.viewDetails)
print >> sys.stderr, "details:", self.viewDetails.grid_info()
def takeScreenshotAndShowItOnWindow(self):
'''
Takes the current screenshot and shows it on the main window.
It also:
- sizes the window
- create the canvas
- set the focus
- enable the events
- create widgets
- finds the targets (as explained in L{findTargets})
- hides the vignette (that could have been showed before)
'''
if PROFILE:
print >> sys.stderr, "PROFILING: takeScreenshotAndShowItOnWindow()"
profileStart()
if DEBUG:
print >> sys.stderr, "takeScreenshotAndShowItOnWindow()"
if self.vc and self.vc.uiAutomatorHelper:
received = self.vc.uiAutomatorHelper.takeScreenshot()
stream = StringIO.StringIO(received)
self.unscaledScreenshot = Image.open(stream)
else:
self.unscaledScreenshot = self.device.takeSnapshot(reconnect=True)
self.image = self.unscaledScreenshot
(width, height) = self.image.size
if self.scale != 1:
scaledWidth = int(width * self.scale)
scaledHeight = int(height * self.scale)
self.image = self.image.resize((scaledWidth, scaledHeight), PIL.Image.ANTIALIAS)
(width, height) = self.image.size
if self.isDarwin and 14 < self.sdkVersion < 23:
stream = StringIO.StringIO()
self.image.save(stream, 'GIF')
import base64
gif = base64.b64encode(stream.getvalue())
stream.close()
if self.canvas is None:
if DEBUG:
print >> sys.stderr, "Creating canvas", width, 'x', height
self.placeholder.grid_forget()
self.canvas = Tkinter.Canvas(self.mainFrame, width=width, height=height)
self.canvas.focus_set()
self.enableEvents()
self.createMessageArea(width, height)
self.createVignette(width, height)
if self.isDarwin and self.scale != 1 and 14 < self.sdkVersion < 23:
# Extremely weird Tkinter bug, I guess
# If the image was rotated and then resized if ImageTk.PhotoImage(self.image)
# is used as usual then the result is a completely transparent image and only
# the "Please wait..." is seen.
# Converting it to GIF seems to solve the problem
self.screenshot = Tkinter.PhotoImage(data=gif)
else:
self.screenshot = ImageTk.PhotoImage(self.image)
if self.imageId is not None:
self.canvas.delete(self.imageId)
self.imageId = self.canvas.create_image(0, 0, anchor=Tkinter.NW, image=self.screenshot)
if DEBUG:
try:
print >> sys.stderr, "Grid info", self.canvas.grid_info()
except:
print >> sys.stderr, "Exception getting grid info"
gridInfo = None
try:
gridInfo = self.canvas.grid_info()
except:
if DEBUG:
print >> sys.stderr, "Adding canvas to grid (1,1)"
self.canvas.grid(row=1, column=1, rowspan=4)
if not gridInfo:
self.canvas.grid(row=1, column=1, rowspan=4)
self.findTargets()
self.hideVignette()
if DEBUG:
try:
self.printGridInfo()
except:
pass
if PROFILE:
profileEnd()
def createMessageArea(self, width, height):
self.__message = Tkinter.Label(self.window, text='', background=Color.GOLD, font=('Helvetica', 16),
anchor=Tkinter.W)
self.__message.configure(width=width)
self.__messageAreaId = self.canvas.create_window(0, 0, anchor=Tkinter.NW, window=self.__message)
self.canvas.itemconfig(self.__messageAreaId, state='hidden')
self.isMessageAreaVisible = False
def showMessageArea(self):
if self.__messageAreaId:
self.canvas.itemconfig(self.__messageAreaId, state='normal')
self.isMessageAreaVisible = True
self.canvas.update_idletasks()
def hideMessageArea(self):
if self.__messageAreaId and self.isMessageAreaVisible:
self.canvas.itemconfig(self.__messageAreaId, state='hidden')
self.isMessageAreaVisible = False
self.canvas.update_idletasks()
def toggleMessageArea(self):
if self.isMessageAreaVisible:
self.hideMessageArea()
else:
self.showMessageArea()
def message(self, text, background=None):
self.__message.config(text=text)
if background:
self.__message.config(background=background)
self.showMessageArea()
def toast(self, text, background=None, timeout=5):
if DEBUG:
print >> sys.stderr, "toast(", text, ",", background, ")"
self.message(text, background)
if text:
t = threading.Timer(timeout, self.hideMessageArea)
t.start()
else:
self.hideMessageArea()
def createVignette(self, width, height):
if DEBUG:
print >> sys.stderr, "createVignette(%d, %d)" % (width, height)
self.vignetteId = self.canvas.create_rectangle(0, 0, width, height, fill=Color.MAGENTA,
stipple='gray50')
font = tkFont.Font(family='Helvetica', size=int(144 * self.scale))
msg = "Please\nwait..."
self.waitMessageShadowId = self.canvas.create_text(width / 2 + 2, height / 2 + 2, text=msg,
fill=Color.DARK_GRAY, font=font)
self.waitMessageId = self.canvas.create_text(width / 2, height / 2, text=msg,
fill=Color.LIGHT_GRAY, font=font)
self.canvas.update_idletasks()
def showVignette(self):
if DEBUG:
print >> sys.stderr, "showVignette()"
if self.canvas is None:
return
if self.vignetteId:
if DEBUG:
print >> sys.stderr, " showing vignette"
# disable events while we are processing one
self.disableEvents()
self.canvas.lift(self.vignetteId)
self.canvas.lift(self.waitMessageShadowId)
self.canvas.lift(self.waitMessageId)
self.canvas.update_idletasks()
def hideVignette(self):
if DEBUG:
print >> sys.stderr, "hideVignette()"
if self.canvas is None:
return
if self.vignetteId:
if DEBUG:
print >> sys.stderr, " hiding vignette"
self.canvas.lift(self.imageId)
self.canvas.update_idletasks()
self.enableEvents()
def deleteVignette(self):
if self.canvas is not None:
self.canvas.delete(self.vignetteId)
self.vignetteId = None
self.canvas.delete(self.waitMessageShadowId)
self.waitMessageShadowId = None
self.canvas.delete(self.waitMessageId)
self.waitMessageId = None
def showPopupMenu(self, event):
(scaledX, scaledY) = (event.x / self.scale, event.y / self.scale)
v = self.findViewContainingPointInTargets(scaledX, scaledY)
ContextMenu(self, view=v).showPopupMenu(event)
def showHelp(self):
d = HelpDialog(self)
self.window.wait_window(d)
def showSideFrame(self):
if not self.isSideFrameShown:
self.sideFrame.grid(row=1, column=2, rowspan=4, sticky=Tkinter.N + Tkinter.S)
self.isSideFrameSown = True
if DEBUG:
self.printGridInfo()
def hideSideFrame(self):
self.sideFrame.grid_forget()
self.isSideFrameShown = False
if DEBUG:
self.printGridInfo()
def showViewTree(self):
self.showSideFrame()
self.viewTree.grid(row=1, column=1, rowspan=3, sticky=Tkinter.N + Tkinter.S)
self.isViewTreeShown = True
if DEBUG:
self.printGridInfo()
def hideViewTree(self):
self.unmarkTargets()
self.viewTree.grid_forget()
self.isViewTreeShown = False
if not self.isViewDetailsShown:
self.hideSideFrame()
if DEBUG:
self.printGridInfo()
def showViewDetails(self):
self.showSideFrame()
row = 4
# if self.viewTree.grid_info() != {}:
# row += 1
self.viewDetails.grid(row=row, column=1, rowspan=1, sticky=Tkinter.S)
self.isViewDetailsShown = True
if DEBUG:
self.printGridInfo()
def hideViewDetails(self):
self.viewDetails.grid_forget()
self.isViewDetailsShown = False
if not self.isViewTreeShown:
self.hideSideFrame()
if DEBUG:
self.printGridInfo()
def viewTreeItemClicked(self, event):
if DEBUG:
print >> sys.stderr, "viewTreeitemClicked:", event.__dict__
self.unmarkTargets()
vuid = self.viewTree.viewTree.identify_row(event.y)
if vuid:
view = self.vc.viewsById[vuid]
if view:
coords = view.getCoords()
if view.isTarget():
self.markTarget(coords[0][0], coords[0][1], coords[1][0], coords[1][1])
self.viewDetails.set(view)
def populateViewTree(self, view):
'''
Populates the View tree.
'''
vuid = view.getUniqueId()
text = view.__smallStr__()
if view.getParent() is None:
self.viewTree.insert('', Tkinter.END, vuid, text=text)
else:
self.viewTree.insert(view.getParent().getUniqueId(), Tkinter.END, vuid, text=text, tags=('ttk'))
self.viewTree.set(vuid, 'T', '*' if view.isTarget() else ' ')
self.viewTree.tag_bind('ttk', '<1>', self.viewTreeItemClicked)
def findTargets(self):
'''
Finds the target Views (i.e. for touches).
'''
if DEBUG:
print >> sys.stderr, "findTargets()"
LISTVIEW_CLASS = 'android.widget.ListView'
''' The ListView class name '''
self.targets = []
''' The list of target coordinates (x1, y1, x2, y2) '''
self.targetViews = []
''' The list of target Views '''
if CHECK_KEYBOARD_SHOWN:
if self.device.isKeyboardShown():
print >> sys.stderr, "#### keyboard is show but handling it is not implemented yet ####"
# FIXME: still no windows in uiautomator
window = -1
else:
window = -1
else:
window = -1
if self.vc:
dump = self.vc.dump(window=window, sleep=0.1)
self.printOperation(None, Operation.DUMP, window, dump)
else:
dump = []
# the root element cannot be deleted from Treeview once added.
# We have no option but to recreate it
self.viewTree = ViewTree(self.sideFrame)
for v in dump:
if DEBUG:
print >> sys.stderr, " findTargets: analyzing", v.getClass(), v.getId()
if v.getClass() == LISTVIEW_CLASS:
# We may want to touch ListView elements, not just the ListView
continue
parent = v.getParent()
if (parent and parent.getClass() == LISTVIEW_CLASS and self.isClickableCheckableOrFocusable(parent)) \
or self.isClickableCheckableOrFocusable(v):
# If this is a touchable ListView, let's add its children instead
# or add it if it's touchable, focusable, whatever
((x1, y1), (x2, y2)) = v.getCoords()
if DEBUG:
print >> sys.stderr, "appending target", ((x1, y1, x2, y2))
v.setTarget(True)
self.targets.append((x1, y1, x2, y2))
self.targetViews.append(v)
target = True
else:
target = False
if self.vc:
self.vc.traverse(transform=self.populateViewTree)
def getViewContainingPointAndGenerateTestCondition(self, x, y):
if DEBUG:
print >> sys.stderr, 'getViewContainingPointAndGenerateTestCondition(%d, %d)' % (x, y)
self.finishGeneratingTestCondition()
vlist = self.vc.findViewsContainingPoint((x, y))
vlist.reverse()
for v in vlist:
text = v.getText()
if text:
self.toast(u'Asserting view with text=%s' % text, timeout=5)
# FIXME: only getText() is invoked by the generated assert(), a parameter
# should be used to provide different alternatives to printOperation()
self.printOperation(v, Operation.TEST, text)
break
def findViewContainingPointInTargets(self, x, y):
if self.vc:
vlist = self.vc.findViewsContainingPoint((x, y))
if DEBUG_FIND_VIEW:
print >> sys.stderr, "Views found:"
for v in vlist:
print >> sys.stderr, " ", v.__smallStr__()
vlist.reverse()
for v in vlist:
if DEBUG:
print >> sys.stderr, "checking if", v, "is in", self.targetViews
if v in self.targetViews:
if DEBUG_TOUCH:
print >> sys.stderr
print >> sys.stderr, "I guess you are trying to touch:", v
print >> sys.stderr
return v
return None
def getViewContainingPointAndTouch(self, x, y):
if DEBUG:
print >> sys.stderr, 'getViewContainingPointAndTouch(%d, %d)' % (x, y)
if self.areEventsDisabled:
if DEBUG:
print >> sys.stderr, "Ignoring event"
self.canvas.update_idletasks()
return
self.showVignette()
if DEBUG_POINT:
print >> sys.stderr, "getViewsContainingPointAndTouch(x=%s, y=%s)" % (x, y)
print >> sys.stderr, "self.vc=", self.vc
v = self.findViewContainingPointInTargets(x, y)
if v is None:
self.hideVignette()
msg = "There are no explicitly touchable or clickable views here! Touching with [x,y]"
self.toast(msg)
# A partial hack which temporarily toggles touch point
self.toggleTouchPoint()
self.touchPoint(x, y)
self.toggleTouchPoint()
else:
if self.vc.uiAutomatorHelper:
# These operations are only available through uiAutomatorHelper
if v == self.vc.navBack:
self.pressBack()
return
elif v == self.vc.navHome:
self.pressHome()
return
elif v == self.vc.navRecentApps:
self.pressRecentApps()
return
clazz = v.getClass()
if clazz == 'android.widget.EditText':
title = "EditText"
kwargs = {}
if DEBUG:
print >> sys.stderr, v
if v.isPassword():
title = "Password"
kwargs = {'show': '*'}
text = tkSimpleDialog.askstring(title, "Enter text to type into this field", **kwargs)
self.canvas.focus_set()
if text:
self.vc.setText(v, text)
self.printOperation(v, Operation.SET_TEXT, text)
else:
self.hideVignette()
return
else:
candidates = [v]
def findBestCandidate(view):
isccf = Culebron.isClickableCheckableOrFocusable(view)
cd = view.getContentDescription()
text = view.getText()
if (cd or text) and not isccf:
# because isccf==False this view was not added to the list of targets
# (i.e. Settings)
candidates.insert(0, view)
return None
if not (v.getText() or v.getContentDescription()) and v.getChildren():
self.vc.traverse(root=v, transform=findBestCandidate, stream=None)
if len(candidates) > 2:
warnings.warn("We are in trouble, we have more than one candidate to touch", stacklevel=0)
candidate = candidates[0]
self.touchView(candidate, v if candidate != v else None)
self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
self.vc.sleep(5)
self.takeScreenshotAndShowItOnWindow()
def pressBack(self):
self.showVignette()
self.vc.pressBack()
if self.vc.uiAutomatorHelper:
self.printOperation(None, Operation.PRESS_BACK_UI_AUTOMATOR_HELPER)
else:
self.printOperation(None, Operation.PRESS_BACK)
self.takeScreenshotAndShowItOnWindow()
def pressHome(self):
self.showVignette()
self.vc.pressHome()
if self.vc.uiAutomatorHelper:
self.printOperation(None, Operation.PRESS_HOME_UI_AUTOMATOR_HELPER)
else:
self.printOperation(None, Operation.PRESS_HOME)
self.takeScreenshotAndShowItOnWindow()
def pressRecentApps(self):
self.showVignette()
self.vc.pressRecentApps()
if self.vc.uiAutomatorHelper:
self.printOperation(None, Operation.PRESS_RECENT_APPS_UI_AUTOMATOR_HELPER)
else:
self.printOperation(None, Operation.PRESS_RECENT_APPS)
self.vc.sleep(1)
self.takeScreenshotAndShowItOnWindow()
def setText(self, v, text):
if DEBUG:
print >> sys.stderr, "setText(%s, '%s')" % (v.__tinyStr__(), text)
# This is deleting the existing text, which should be asked in the dialog, but I would have to implement
# the dialog myself
v.setText(text)
# This is not deleting the text, so appending if there's something
# v.type(text)
self.printOperation(v, Operation.TYPE, text)
def touchView(self, v, root=None):
v.touch()
if v.uiAutomatorHelper:
self.printOperation(v, Operation.TOUCH_VIEW_UI_AUTOMATOR_HELPER, v.obtainSelectorForView())
else:
# we pass root=v as an argument so the corresponding findView*() searches in this
# subtree instead of the full tree
self.printOperation(v, Operation.TOUCH_VIEW, root)
def touchPoint(self, x, y):
'''
Touches a point in the device screen.
The generated operation will use the units specified in L{coordinatesUnit} and the
orientation in L{vc.display['orientation']}.
'''
if DEBUG:
print >> sys.stderr, 'touchPoint(%d, %d)' % (x, y)
print >> sys.stderr, 'touchPoint:', type(x), type(y)
if self.areEventsDisabled:
if DEBUG:
print >> sys.stderr, "Ignoring event"
self.canvas.update_idletasks()
return
if DEBUG:
print >> sys.stderr, "Is touching point:", self.isTouchingPoint
if self.isTouchingPoint:
self.showVignette()
self.vc.touch(x, y)
if self.coordinatesUnit == Unit.DIP:
x = round(x / self.device.display['density'], 2)
y = round(y / self.device.display['density'], 2)
self.printOperation(None, Operation.TOUCH_POINT, x, y, self.coordinatesUnit,
self.device.display['orientation'])
self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
# FIXME: can we reduce this sleep? (was 5)
time.sleep(1)
self.isTouchingPoint = self.vc is None
self.takeScreenshotAndShowItOnWindow()
# self.hideVignette()
self.statusBar.clear()
return
def longTouchPoint(self, x, y):
'''
Long-touches a point in the device screen.
The generated operation will use the units specified in L{coordinatesUnit} and the
orientation in L{vc.display['orientation']}.
'''
if DEBUG:
print >> sys.stderr, 'longTouchPoint(%d, %d)' % (x, y)
if self.areEventsDisabled:
if DEBUG:
print >> sys.stderr, "Ignoring event"
self.canvas.update_idletasks()
return
if DEBUG:
print >> sys.stderr, "Is long touching point:", self.isLongTouchingPoint
if self.isLongTouchingPoint:
self.showVignette()
self.vc.longTouch(x, y)
if self.coordinatesUnit == Unit.DIP:
x = round(x / self.device.display['density'], 2)
y = round(y / self.device.display['density'], 2)
self.printOperation(None, Operation.LONG_TOUCH_POINT, x, y, 2000, self.coordinatesUnit,
self.device.display['orientation'])
self.printOperation(None, Operation.SLEEP, 5)
time.sleep(5)
self.isLongTouchingPoint = False
self.takeScreenshotAndShowItOnWindow()
# self.hideVignette()
self.statusBar.clear()
return
def longTouchView(self, v, root=None):
v.longTouch()
if v.uiAutomatorHelper:
self.printOperation(v, Operation.LONG_TOUCH_VIEW_UI_AUTOMATOR_HELPER, v.obtainSelectorForView())
else:
# we pass root=v as an argument so the corresponding findView*() searches in this
# subtree instead of the full tree
self.printOperation(v, Operation.LONG_TOUCH_VIEW, root)
self.isLongTouchingView = False
def onButton1Pressed(self, event):
if DEBUG:
print >> sys.stderr, "onButton1Pressed((", event.x, ", ", event.y, "))"
(scaledX, scaledY) = (event.x / self.scale, event.y / self.scale)
if DEBUG:
print >> sys.stderr, " onButton1Pressed: scaled: (", scaledX, ", ", scaledY, ")"
print >> sys.stderr, " onButton1Pressed: is grabbing:", self.isGrabbingTouch
if self.isGrabbingTouch:
self.onTouchListener((scaledX, scaledY))
self.isGrabbingTouch = False
elif self.isDragDialogShowed:
self.toast("No touch events allowed while setting drag parameters", background=Color.GOLD)
return
elif self.isTouchingPoint:
self.touchPoint(scaledX, scaledY)
elif self.isLongTouchingPoint:
self.longTouchPoint(scaledX, scaledY)
elif self.isLongTouchingView:
self.getViewContainingPointAndLongTouch(scaledX, scaledY)
elif self.isGeneratingTestCondition:
self.getViewContainingPointAndGenerateTestCondition(scaledX, scaledY)
else:
if self.vc:
self.getViewContainingPointAndTouch(scaledX, scaledY)
else:
# If we don't have Views, there no other option than touching points
self.touchPoint(scaledX, scaledY)
def onCtrlButton1Pressed(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlButton1Pressed((", event.x, ", ", event.y, "))"
(scaledX, scaledY) = (event.x / self.scale, event.y / self.scale)
l = self.vc.findViewsContainingPoint((scaledX, scaledY))
if l and len(l) > 0:
self.saveViewSnapshot(l[-1])
else:
msg = "There are no views here!"
self.toast(msg)
return
def onButton2Pressed(self, event):
if DEBUG:
print >> sys.stderr, "onButton2Pressed((", event.x, ", ", event.y, "))"
osName = platform.system()
if osName == 'Darwin':
self.showPopupMenu(event)
def onButton3Pressed(self, event):
if DEBUG:
print >> sys.stderr, "onButton3Pressed((", event.x, ", ", event.y, "))"
self.showPopupMenu(event)
def command(self, keycode):
'''
Presses a key.
Generates the actual key press on the device and prints the line in the script.
'''
self.device.press(keycode)
self.printOperation(None, Operation.PRESS, keycode)
def onKeyPressed(self, event):
if DEBUG_KEY:
print >> sys.stderr, "onKeyPressed(", repr(event), ")"
print >> sys.stderr, " event", type(event.char), len(event.char), repr(
event.char), event.keysym, event.keycode, event.type
print >> sys.stderr, " events disabled:", self.areEventsDisabled
if self.areEventsDisabled:
if DEBUG_KEY:
print >> sys.stderr, "ignoring event"
self.canvas.update_idletasks()
return
char = event.char
keysym = event.keysym
if len(char) == 0 and not (
keysym in Culebron.KEYSYM_TO_KEYCODE_MAP or keysym in Culebron.KEYSYM_CULEBRON_COMMANDS):
if DEBUG_KEY:
print >> sys.stderr, "returning because len(char) == 0"
return
###
### internal commands: no output to generated script
###
try:
handler = getattr(self, 'onCtrl%s' % self.UPPERCASE_CHARS[ord(char) - 1])
except:
handler = None
if handler:
return handler(event)
elif keysym == 'F1':
self.showHelp()
return
elif keysym == 'F5':
self.refresh()
return
elif keysym == 'F8':
self.printGridInfo()
return
elif keysym == 'Alt_L':
return
elif keysym == 'Control_L':
return
elif keysym == 'Escape':
# we cannot send Escape to the device, but I think it's fine
self.cancelOperation()
return
### empty char (modifier) ###
# here does not process events like Home where char is ''
# if char == '':
# return
###
### target actions
###
self.showVignette()
if keysym in Culebron.KEYSYM_TO_KEYCODE_MAP:
if DEBUG_KEY:
print >> sys.stderr, "Pressing", Culebron.KEYSYM_TO_KEYCODE_MAP[keysym]
self.command(Culebron.KEYSYM_TO_KEYCODE_MAP[keysym])
elif char == '\r':
self.command('ENTER')
elif char == '':
# do nothing
pass
else:
self.command(char.decode('ascii', errors='replace'))
# commented out (profile)
# time.sleep(1)
self.takeScreenshotAndShowItOnWindow()
def wake(self):
self.refresh()
self.printOperation(None, Operation.WAKE)
def refresh(self):
self.showVignette()
self.device.wake()
display = copy.copy(self.device.display)
self.device.initDisplayProperties()
changed = False
for prop in display:
if display[prop] != self.device.display[prop]:
changed = True
break
if changed:
self.window.geometry('%dx%d' % (self.device.display['width'] * self.scale,
self.device.display['height'] * self.scale + int(
self.statusBar.winfo_height())))
self.deleteVignette()
self.canvas.destroy()
self.canvas = None
self.window.update_idletasks()
self.takeScreenshotAndShowItOnWindow()
def cancelOperation(self):
'''
Cancels the ongoing operation if any.
'''
if self.isLongTouchingPoint:
self.toggleLongTouchPoint()
elif self.isTouchingPoint:
self.toggleTouchPoint()
elif self.isGeneratingTestCondition:
self.toggleGenerateTestCondition()
def printStartActivityAtTop(self):
self.printOperation(None, Operation.START_ACTIVITY, self.device.getTopActivityName())
def onCtrlA(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlA(", event, ")"
self.printStartActivityAtTop()
def showDragDialog(self):
d = DragDialog(self)
self.window.wait_window(d)
self.setDragDialogShowed(False)
def onCtrlD(self, event):
self.showDragDialog()
def onCtrlF(self, event):
self.saveSnapshot()
def saveSnapshot(self):
'''
Saves the current shanpshot to the specified file.
Current snapshot is the image being displayed on the main window.
'''
filename = self.snapshotDir + os.sep + '${serialno}-${focusedwindowname}-${timestamp}' + '.' + self.snapshotFormat.lower()
# We have the snapshot already taken, no need to retake
d = FileDialog(self, self.device.substituteDeviceTemplate(filename))
saveAsFilename = d.askSaveAsFilename()
if saveAsFilename:
_format = os.path.splitext(saveAsFilename)[1][1:].upper()
self.printOperation(None, Operation.SNAPSHOT, filename, _format, self.deviceArt, self.dropShadow,
self.screenGlare)
# FIXME: we should add deviceArt, dropShadow and screenGlare to the saved image
# self.unscaledScreenshot.save(saveAsFilename, _format, self.deviceArt, self.dropShadow, self.screenGlare)
self.unscaledScreenshot.save(saveAsFilename, _format)
def saveViewSnapshot(self, view):
'''
Saves the View snapshot.
'''
if not view:
raise ValueError("view must be provided to take snapshot")
filename = self.snapshotDir + os.sep + '${serialno}-' + view.variableNameFromId() + '-${timestamp}' + '.' + self.snapshotFormat.lower()
d = FileDialog(self, self.device.substituteDeviceTemplate(filename))
saveAsFilename = d.askSaveAsFilename()
if saveAsFilename:
_format = os.path.splitext(saveAsFilename)[1][1:].upper()
self.printOperation(view, Operation.VIEW_SNAPSHOT, filename, _format)
view.writeImageToFile(saveAsFilename, _format)
def toggleTouchPointDip(self):
'''
Toggles the touch point operation using L{Unit.DIP}.
This invokes L{toggleTouchPoint}.
'''
self.coordinatesUnit = Unit.DIP
self.toggleTouchPoint()
def onCtrlI(self, event):
self.toggleTouchPointDip()
def toggleLongTouchPoint(self):
'''
Toggles the long touch point operation.
'''
if not self.isLongTouchingPoint:
msg = 'Long touching point'
self.toast(msg, background=Color.GREEN)
self.statusBar.set(msg)
self.isLongTouchingPoint = True
# FIXME: There should be 2 methods DIP & PX
self.coordinatesUnit = Unit.PX
else:
self.toast(None)
self.statusBar.clear()
self.isLongTouchingPoint = False
def toggleLongTouchView(self):
'''
Toggles the long touch View operation.
:return:
'''
if not self.isLongTouchingView:
msg = 'Long touching View'
self.toast(msg, background=Color.GREEN)
self.statusBar.set(msg)
self.isLongTouchingView = True
else:
self.toast(None)
self.statusBar.clear()
self.isLongTouchingView = False
def onCtrlL(self, event):
self.toggleLongTouchPoint()
def toggleTouchPoint(self):
'''
Toggles the touch point operation using the units specified in L{coordinatesUnit}.
When there are L{View}s (obtained from the back-end) we have to determine if the
intention when something is touched on the window if we want to touch the L{View}
or the point.
If there's no back-end, we don't allow L{self.isTouchingPoint} to be disabled so we will
never be attempting to touch L{View}s.
'''
if not self.isTouchingPoint:
msg = 'Touching point (units=%s)' % self.coordinatesUnit
self.toast(msg, background=Color.GREEN)
self.statusBar.set(msg)
self.isTouchingPoint = True
else:
self.toast(None)
self.statusBar.clear()
self.isTouchingPoint = self.vc is None
def toggleTouchPointPx(self):
self.coordinatesUnit = Unit.PX
self.toggleTouchPoint()
def onCtrlP(self, event):
self.toggleTouchPointPx()
def onCtrlQ(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlQ(%s)" % event
self.quit()
def quit(self):
if self.vc.uiAutomatorHelper:
if DEBUG or True:
print >> sys.stderr, "Quitting UiAutomatorHelper..."
self.vc.uiAutomatorHelper.quit()
self.window.destroy()
def showSleepDialog(self):
seconds = tkSimpleDialog.askfloat('Sleep Interval', 'Value in seconds:', initialvalue=1, minvalue=0,
parent=self.window)
if seconds is not None:
self.printOperation(None, Operation.SLEEP, seconds)
self.canvas.focus_set()
def onCtrlS(self, event):
self.showSleepDialog()
def startGeneratingTestCondition(self):
self.message('Generating test condition...', background=Color.GREEN)
self.isGeneratingTestCondition = True
def finishGeneratingTestCondition(self):
self.isGeneratingTestCondition = False
self.hideMessageArea()
def toggleGenerateTestCondition(self):
'''
Toggles generating test condition
'''
if self.vc is None:
self.toast('Test conditions can be generated when a back-end is defined')
return
if self.isGeneratingTestCondition:
self.finishGeneratingTestCondition()
else:
self.startGeneratingTestCondition()
def onCtrlT(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlT()"
if self.vc is None:
self.toast('Test conditions can be generated when a back-end is defined')
return
# FIXME: This is only valid if we are generating a test case
self.toggleGenerateTestCondition()
def onCtrlU(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlU()"
def onCtrlV(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlV()"
self.printOperation(None, Operation.TRAVERSE)
def toggleTargetZones(self):
self.toggleTargets()
self.canvas.update_idletasks()
def onCtrlZ(self, event):
if DEBUG:
print >> sys.stderr, "onCtrlZ()"
self.toggleTargetZones()
def showControlPanel(self):
from com.dtmilano.android.controlpanel import ControlPanel
self.controlPanel = ControlPanel(self, self.printOperation)
def onCtrlK(self, event):
self.showControlPanel()
def drag(self, start, end, duration, steps, units=Unit.DIP):
self.showVignette()
# the operation on this current device is always done in PX
# so let's do it before any conversion takes place
self.device.drag(start, end, duration, steps)
if units == Unit.DIP:
x0 = round(start[0] / self.device.display['density'], 2)
y0 = round(start[1] / self.device.display['density'], 2)
x1 = round(end[0] / self.device.display['density'], 2)
y1 = round(end[1] / self.device.display['density'], 2)
start = (x0, y0)
end = (x1, y1)
if self.vc.uiAutomatorHelper:
self.printOperation(None, Operation.SWIPE_UI_AUTOMATOR_HELPER, x0, y0, x1, y1, steps, units,
self.device.display['orientation'])
else:
self.printOperation(None, Operation.DRAG, start, end, duration, steps, units,
self.device.display['orientation'])
self.printOperation(None, Operation.SLEEP, 1)
time.sleep(1)
self.takeScreenshotAndShowItOnWindow()
def enableEvents(self):
if self.permanentlyDisableEvents:
return
self.canvas.update_idletasks()
self.canvas.bind("<Button-1>", self.onButton1Pressed)
self.canvas.bind("<Control-Button-1>", self.onCtrlButton1Pressed)
self.canvas.bind("<Button-2>", self.onButton2Pressed)
self.canvas.bind("<Button-3>", self.onButton3Pressed)
self.canvas.bind("<BackSpace>", self.onKeyPressed)
# self.canvas.bind("<Control-Key-S>", self.onCtrlS)
self.canvas.bind("<Key>", self.onKeyPressed)
self.areEventsDisabled = False
def disableEvents(self, permanently=False):
self.permanentlyDisableEvents = permanently
if self.canvas is not None:
self.canvas.update_idletasks()
self.areEventsDisabled = True
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<Control-Button-1>")
self.canvas.unbind("<Button-2>")
self.canvas.unbind("<Button-3>")
self.canvas.unbind("<BackSpace>")
# self.canvas.unbind("<Control-Key-S>")
self.canvas.unbind("<Key>")
def toggleTargets(self):
if DEBUG:
print >> sys.stderr, "toggletargets: aretargetsmarked=", self.areTargetsMarked
if not self.areTargetsMarked:
self.markTargets()
else:
self.unmarkTargets()
def markTargets(self):
if DEBUG:
print >> sys.stderr, "marktargets: aretargetsmarked=", self.areTargetsMarked
print >> sys.stderr, " marktargets: targets=", self.targets
colors = ["#ff00ff", "#ffff00", "#00ffff"]
self.markedTargetIds = {}
c = 0
for (x1, y1, x2, y2) in self.targets:
if DEBUG:
print "adding rectangle:", x1, y1, x2, y2
self.markTarget(x1, y1, x2, y2, colors[c % len(colors)])
c += 1
self.areTargetsMarked = True
def markTarget(self, x1, y1, x2, y2, color='#ff00ff'):
'''
@return the id of the rectangle added
'''
# self.areTargetsMarked = True
_id = self.canvas.create_rectangle(x1 * self.scale, y1 * self.scale, x2 * self.scale, y2 * self.scale,
fill=color,
stipple="gray25")
self.markedTargetIds[_id] = (x1, y1, x2, y2)
return _id
def unmarkTarget(self, _id):
self.canvas.delete(_id)
def unmarkTargets(self):
if not self.areTargetsMarked:
return
for _id in self.markedTargetIds:
self.unmarkTarget(_id)
self.markedTargetIds = {}
self.areTargetsMarked = False
def setDragDialogShowed(self, showed):
self.isDragDialogShowed = showed
if showed:
pass
else:
self.isGrabbingTouch = False
def drawTouchedPoint(self, x, y):
if DEBUG:
print >> sys.stderr, "drawTouchedPoint(", x, ",", y, ")"
size = 50
return self.canvas.create_oval((x - size) * self.scale, (y - size) * self.scale, (x + size) * self.scale,
(y + size) * self.scale, fill=Color.MAGENTA)
def drawDragLine(self, x0, y0, x1, y1):
if DEBUG:
print >> sys.stderr, "drawDragLine(", x0, ",", y0, ",", x1, ",", y1, ")"
width = 15
return self.canvas.create_line(x0 * self.scale, y0 * self.scale, x1 * self.scale, y1 * self.scale, width=width,
fill=Color.MAGENTA, arrow="last", arrowshape=(50, 50, 30), dash=(50, 25))
def executeCommandAndRefresh(self, command):
self.showVignette()
if DEBUG:
print >> sys.stderr, 'DEBUG: command=', command, command.__name__
print >> sys.stderr, 'DEBUG: command=', command.__self__, command.__self__.view
try:
view = command.__self__.view
except AttributeError:
view = None
# FIXME: If we are not dumping the Views and assigning to variables (i.e -u was used on command line) then
# when we try to do an operation on the View via its variable name it's going to fail when the saved script
# is executed
self.printOperation(view, Operation.fromCommandName(command.__name__))
command()
self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
self.vc.sleep(5)
# FIXME: perhaps refresh() should be invoked here just in case size or orientation changed
self.takeScreenshotAndShowItOnWindow()
def changeLanguage(self):
code = tkSimpleDialog.askstring("Change language", "Enter the language code")
self.vc.uiDevice.changeLanguage(code)
self.printOperation(None, Operation.CHANGE_LANGUAGE, code)
self.refresh()
def setOnTouchListener(self, listener):
self.onTouchListener = listener
def setGrab(self, state):
if DEBUG:
print >> sys.stderr, "Culebron.setGrab(%s)" % state
if state and not self.onTouchListener:
warnings.warn('Starting to grab but no onTouchListener')
self.isGrabbingTouch = state
if state:
self.toast('Grabbing drag points...', background=Color.GREEN)
else:
self.hideMessageArea()
@staticmethod
def isClickableCheckableOrFocusable(v):
if DEBUG_ISCCOF:
print >> sys.stderr, "isClickableCheckableOrFocusable(", v.__tinyStr__(), ")"
try:
if not v.isEnabled():
# if not enabled, then it cannot be a target
return False
except AttributeError:
pass
try:
return v.isClickable()
except AttributeError:
pass
try:
return v.isCheckable()
except AttributeError:
pass
try:
return v.isFocusable()
except AttributeError:
pass
return False
def mainloop(self):
self.window.title("%s v%s" % (Culebron.APPLICATION_NAME, __version__))
self.window.resizable(width=Tkinter.FALSE, height=Tkinter.FALSE)
self.window.lift()
if self.concertina:
self.concertinaLoop()
else:
self.window.mainloop()
def concertinaLoop(self):
random.seed()
self.disableEvents(permanently=True)
self.concertinaLoopCallback(dontinteract=True)
self.window.mainloop()
def concertinaLoopCallback(self, dontinteract=False):
if not dontinteract:
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: should select one of these targets:"
for v in self.targetViews:
print >> sys.stderr, " ", unicode(v.__tinyStr__())
rand = random.random()
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: random=%f" % rand
if rand > 0.85:
# Send key events
k = random.choice(['ENTER', 'BACK', 'HOME', 'MENU'])
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: key=" + k
# DEBUG ONLY!
# print >> sys.stderr, "Not sending key event"
self.command(k)
else:
# Act on views
_len = len(self.targetViews)
if _len > 0:
i = random.randrange(len(self.targetViews))
target = self.targetViews[i]
z = self.targets[i]
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: selected", unicode(target.__smallStr__())
print >> sys.stderr, "CONCERTINA: selected", z
_id = self.markTarget(*z)
self.window.update_idletasks()
time.sleep(1)
self.unmarkTarget(_id)
self.window.update_idletasks()
clazz = target.getClass()
parent = target.getParent()
if parent:
parentClass = parent.getClass()
else:
parentClass = None
isScrollable = target.isScrollable()
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: is scrollable: ", isScrollable
if parent:
print >> sys.stderr, "CONCERTINA: is scrollable parent: ", parent.isScrollable()
# cond = (isScrollable or parent.isScrollable() or parentClass == 'android.widget.ScrollView')
# DEBUG ONLY!
# print >> sys.stderr, "CONCERTINA: check:", cond
# if not cond:
# self.window.after(500, self.concertinaLoopCallback)
# return
if clazz == 'android.widget.EditText':
id = target.getId()
txt = target.getText()
if target.isPassword() or re.search('password', id, re.IGNORECASE) or re.search('password', txt,
re.IGNORECASE):
text = Concertina.getRandomPassword()
elif re.search('email', id, re.IGNORECASE) or re.search('email', txt, re.IGNORECASE):
text = Concertina.getRandomEmail()
else:
text = Concertina.getRandomText()
if DEBUG_CONCERTINA:
print >> sys.stderr, "Entering text: ", text
if not text:
raise RuntimeError('text is None')
self.setText(target, text)
elif target.getContentDescription() in ['Voice Search', 'Tap to speak']:
Concertina.sayRandomText()
time.sleep(5)
elif random.choice(['SCROLL', 'TOUCH']) == 'SCROLL' and (
isScrollable or parent.isScrollable() or parentClass == 'android.widget.ScrollView'):
# NOTE: The order here is important because some EditText are inside ScrollView's and we want to
# capture the case of other ScrollViews
if isScrollable:
((l, t), (r, b)) = target.getBounds()
else:
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: using parent bounds because it's scrollable"
((l, t), (r, b)) = parent.getBounds()
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: bounds=", ((l, t), (r, b))
if random.choice(['VERTICAL', 'HORIZONTAL']) == 'VERTICAL':
if DEBUG_CONCERTINA:
print >> sys.stderr, 'CONCERTINA: VERTICAL'
sp = (l + (r - l) / 2, t + 50)
ep = (l + (r - l) / 2, b - 50)
else:
if DEBUG_CONCERTINA:
print >> sys.stderr, 'CONCERTINA: HORIZONTAL'
sp = (l + 50, t + (b - t) / 2)
ep = (r - 50, t + (b - t) / 2)
if random.choice(['FORWARD', 'REVERSE']) == 'REVERSE':
if DEBUG_CONCERTINA:
print >> sys.stderr, 'CONCERTINA: REVERSE'
temp = sp
sp = ep
ep = temp
else:
if DEBUG_CONCERTINA:
print >> sys.stderr, 'CONCERTINA: FORWARD'
d = 500
s = 20
_id = self.canvas.create_rectangle(l * self.scale, t * self.scale, r * self.scale,
b * self.scale,
fill="#00ffff", stipple="gray12")
self.window.update_idletasks()
units = Unit.PX
self.drawTouchedPoint(sp[0], sp[1])
self.window.update_idletasks()
self.drawDragLine(sp[0], sp[1], ep[0], ep[1])
self.window.update_idletasks()
time.sleep(5)
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: dragging %s %s %s %s %s" % (sp, ep, d, s, units)
self.drag(sp, ep, d, s, units)
else:
self.touchView(target)
self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: waiting 5 secs"
time.sleep(5)
if DEBUG_CONCERTINA:
print >> sys.stderr, "CONCERTINA: taking screenshot"
self.takeScreenshotAndShowItOnWindow()
else:
print >> sys.stderr, "CONCERTINA: No target views"
self.window.after(5000, self.concertinaLoopCallback)
def getViewContainingPointAndLongTouch(self, x, y):
# FIXME: this method is almost exactly as getViewContainingPointAndTouch()
if DEBUG:
print >> sys.stderr, 'getViewContainingPointAndLongTouch(%d, %d)' % (x, y)
if self.areEventsDisabled:
if DEBUG:
print >> sys.stderr, "Ignoring event"
self.canvas.update_idletasks()
return
self.showVignette()
if DEBUG_POINT:
print >> sys.stderr, "getViewsContainingPointAndLongTouch(x=%s, y=%s)" % (x, y)
print >> sys.stderr, "self.vc=", self.vc
v = self.findViewContainingPointInTargets(x, y)
if v is None:
# FIXME: We can touch by DIP by default if no Views were found
self.hideVignette()
msg = "There are no touchable or clickable views here!"
self.toast(msg)
return
clazz = v.getClass()
candidates = [v]
def findBestCandidate(view):
isccf = Culebron.isClickableCheckableOrFocusable(view)
cd = view.getContentDescription()
text = view.getText()
if (cd or text) and not isccf:
# because isccf==False this view was not added to the list of targets
# (i.e. Settings)
candidates.insert(0, view)
return None
if not (v.getText() or v.getContentDescription()) and v.getChildren():
self.vc.traverse(root=v, transform=findBestCandidate, stream=None)
if len(candidates) > 2:
warnings.warn("We are in trouble, we have more than one candidate to touch", stacklevel=0)
candidate = candidates[0]
self.longTouchView(candidate, v if candidate != v else None)
self.printOperation(None, Operation.SLEEP, Operation.DEFAULT)
self.vc.sleep(5)
self.takeScreenshotAndShowItOnWindow()
if TKINTER_AVAILABLE:
class MainMenu(Tkinter.Menu):
def __init__(self, culebron):
Tkinter.Menu.__init__(self, culebron.window)
self.culebron = culebron
self.fileMenu = Tkinter.Menu(self, tearoff=False)
self.fileMenu.add_command(label="Quit", underline=0, accelerator='Command-Q', command=self.culebron.quit)
self.add_cascade(label="File", underline=0, menu=self.fileMenu)
self.viewMenu = Tkinter.Menu(self, tearoff=False)
self.showViewTree = Tkinter.BooleanVar()
self.showViewTree.set(False)
state = NORMAL if culebron.vc else DISABLED
self.viewMenu.add_checkbutton(label="Tree", underline=0, accelerator='Command-T', onvalue=True,
offvalue=False, variable=self.showViewTree, state=state,
command=self.onshowViewTreeChanged)
self.showViewDetails = Tkinter.BooleanVar()
self.showViewDetails.set(False)
state = NORMAL if culebron.vc else DISABLED
self.viewMenu.add_checkbutton(label="View details", underline=0, accelerator='Command-V', onvalue=True,
offvalue=False, variable=self.showViewDetails, state=state,
command=self.onShowViewDetailsChanged)
self.add_cascade(label="View", underline=0, menu=self.viewMenu)
self.uiDeviceMenu = Tkinter.Menu(self, tearoff=False)
state = NORMAL if culebron.vc else DISABLED
self.uiDeviceMenu.add_command(label="Open Notification", underline=6, state=state,
command=lambda: culebron.executeCommandAndRefresh(
self.culebron.vc.uiDevice.openNotification))
state = NORMAL if culebron.vc else DISABLED
self.uiDeviceMenu.add_command(label="Open Quick settings", underline=6, state=state,
command=lambda: culebron.executeCommandAndRefresh(
command=self.culebron.vc.uiDevice.openQuickSettings))
state = NORMAL if culebron.vc else DISABLED
self.uiDeviceMenu.add_command(label="Change Language", underline=7, state=state,
command=self.culebron.changeLanguage)
self.add_cascade(label="UiDevice", menu=self.uiDeviceMenu)
self.helpMenu = Tkinter.Menu(self, tearoff=False)
self.helpMenu.add_command(label="Keyboard shortcuts", underline=0, accelerator='Command-K',
command=self.culebron.showHelp)
self.add_cascade(label="Help", underline=0, menu=self.helpMenu)
def callback(self):
pass
def onshowViewTreeChanged(self):
if self.showViewTree.get() == 1:
self.culebron.showViewTree()
else:
self.culebron.hideViewTree()
def onShowViewDetailsChanged(self):
if self.showViewDetails.get() == 1:
self.culebron.showViewDetails()
else:
self.culebron.hideViewDetails()
class ViewTree(Tkinter.Frame):
def __init__(self, parent):
Tkinter.Frame.__init__(self, parent)
self.viewTree = ttk.Treeview(self, columns=['T'], height=35)
self.viewTree.column(0, width=20)
self.viewTree.heading('#0', None, text='View', anchor=Tkinter.W)
self.viewTree.heading(0, None, text='T', anchor=Tkinter.W)
self.scrollbar = ttk.Scrollbar(self, orient=Tkinter.HORIZONTAL, command=self.__xscroll)
self.viewTree.grid(row=1, rowspan=1, column=1, sticky=Tkinter.N + Tkinter.S)
self.scrollbar.grid(row=2, rowspan=1, column=1, sticky=Tkinter.E + Tkinter.W)
self.viewTree.configure(xscrollcommand=self.scrollbar.set)
def __xscroll(self, *args):
if DEBUG:
print >> sys.stderr, "__xscroll:", args
self.viewTree.xview(*args)
def insert(self, parent, index, iid=None, **kw):
"""Creates a new item and return the item identifier of the newly
created item.
parent is the item ID of the parent item, or the empty string
to create a new top-level item. index is an integer, or the value
end, specifying where in the list of parent's children to insert
the new item. If index is less than or equal to zero, the new node
is inserted at the beginning, if index is greater than or equal to
the current number of children, it is inserted at the end. If iid
is specified, it is used as the item identifier, iid must not
already exist in the tree. Otherwise, a new unique identifier
is generated."""
return self.viewTree.insert(parent, index, iid, **kw)
def set(self, item, column=None, value=None):
"""With one argument, returns a dictionary of column/value pairs
for the specified item. With two arguments, returns the current
value of the specified column. With three arguments, sets the
value of given column in given item to the specified value."""
return self.viewTree.set(item, column, value)
def tag_bind(self, tagname, sequence=None, callback=None):
if DEBUG:
print >> sys.stderr, 'ViewTree.tag_bind(', tagname, ',', sequence, ',', callback, ')'
return self.viewTree.tag_bind(tagname, sequence, callback)
class ViewDetails(Tkinter.Frame):
VIEW_DETAILS = "View Details:\n"
def __init__(self, parent):
Tkinter.Frame.__init__(self, parent)
self.label = Tkinter.Label(self, bd=1, width=30, wraplength=200, justify=Tkinter.LEFT, anchor=Tkinter.NW)
self.label.configure(text=self.VIEW_DETAILS)
self.label.configure(bg="white")
self.label.grid(row=3, column=1, rowspan=1)
def set(self, view):
self.label.configure(text=self.VIEW_DETAILS + view.__str__())
class StatusBar(Tkinter.Frame):
def __init__(self, parent):
Tkinter.Frame.__init__(self, parent)
self.label = Tkinter.Label(self, bd=1, relief=Tkinter.SUNKEN, anchor=Tkinter.W)
self.label.grid(row=1, column=1, columnspan=2, sticky=Tkinter.E + Tkinter.W)
def set(self, fmt, *args):
self.label.config(text=fmt % args)
self.label.update_idletasks()
def clear(self):
self.label.config(text="")
self.label.update_idletasks()
class LabeledEntry():
def __init__(self, parent, text, validate, validatecmd):
self.f = Tkinter.Frame(parent)
Tkinter.Label(self.f, text=text, anchor="w", padx=8).grid(row=1, column=1, sticky=Tkinter.E)
self.entry = Tkinter.Entry(self.f, validate=validate, validatecommand=validatecmd)
self.entry.grid(row=1, column=2, padx=5, sticky=Tkinter.E)
def grid(self, **kwargs):
self.f.grid(kwargs)
def get(self):
return self.entry.get()
def set(self, text):
self.entry.delete(0, Tkinter.END)
self.entry.insert(0, text)
class LabeledEntryWithButton(LabeledEntry):
def __init__(self, parent, text, buttonText, command, validate, validatecmd):
LabeledEntry.__init__(self, parent, text, validate, validatecmd)
self.button = Tkinter.Button(self.f, text=buttonText, command=command)
self.button.grid(row=1, column=3)
class DragDialog(Tkinter.Toplevel):
DEFAULT_DURATION = 1000
DEFAULT_STEPS = 20
spX = None
spY = None
epX = None
epY = None
spId = None
epId = None
def __init__(self, culebron):
self.culebron = culebron
self.parent = culebron.window
Tkinter.Toplevel.__init__(self, self.parent)
self.transient(self.parent)
self.culebron.setDragDialogShowed(True)
self.title("Drag: selecting parameters")
# valid percent substitutions (from the Tk entry man page)
# %d = Type of action (1=insert, 0=delete, -1 for others)
# %i = index of char string to be inserted/deleted, or -1
# %P = value of the entry if the edit is allowed
# %s = value of entry prior to editing
# %S = the text string being inserted or deleted, if any
# %v = the type of validation that is currently set
# %V = the type of validation that triggered the callback
# (key, focusin, focusout, forced)
# %W = the tk name of the widget
self.validate = (self.parent.register(self.onValidate), '%P')
self.sp = LabeledEntryWithButton(self, "Start point", "Grab", command=self.onGrabSp, validate="focusout",
validatecmd=self.validate)
self.sp.grid(row=1, column=1, columnspan=3, pady=5)
self.ep = LabeledEntryWithButton(self, "End point", "Grab", command=self.onGrabEp, validate="focusout",
validatecmd=self.validate)
self.ep.grid(row=2, column=1, columnspan=3, pady=5)
l = Tkinter.Label(self, text="Units")
l.grid(row=3, column=1, sticky=Tkinter.E)
self.units = Tkinter.StringVar()
self.units.set(Unit.DIP)
col = 2
for u in dir(Unit):
if u.startswith('_'):
continue
rb = Tkinter.Radiobutton(self, text=u, variable=self.units, value=u)
rb.grid(row=3, column=col, padx=20, sticky=Tkinter.E)
col += 1
self.d = LabeledEntry(self, "Duration", validate="focusout", validatecmd=self.validate)
self.d.set(DragDialog.DEFAULT_DURATION)
self.d.grid(row=4, column=1, columnspan=3, pady=5)
self.s = LabeledEntry(self, "Steps", validate="focusout", validatecmd=self.validate)
self.s.set(DragDialog.DEFAULT_STEPS)
self.s.grid(row=5, column=1, columnspan=2, pady=5)
self.buttonBox()
def buttonBox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Tkinter.Frame(self)
self.ok = Tkinter.Button(box, text="OK", width=10, command=self.onOk, default=Tkinter.ACTIVE,
state=Tkinter.DISABLED)
self.ok.grid(row=6, column=1, sticky=Tkinter.E, padx=5, pady=5)
w = Tkinter.Button(box, text="Cancel", width=10, command=self.onCancel)
w.grid(row=6, column=2, sticky=Tkinter.E, padx=5, pady=5)
self.bind("<Return>", self.onOk)
self.bind("<Escape>", self.onCancel)
box.grid(row=6, column=1, columnspan=3)
def onValidate(self, value):
if self.sp.get() and self.ep.get() and self.d.get() and self.s.get():
self.ok.configure(state=Tkinter.NORMAL)
else:
self.ok.configure(state=Tkinter.DISABLED)
def onOk(self, event=None):
if DEBUG:
print >> sys.stderr, "onOK()"
print >> sys.stderr, "values are:",
print >> sys.stderr, self.sp.get(),
print >> sys.stderr, self.ep.get(),
print >> sys.stderr, self.d.get(),
print >> sys.stderr, self.s.get(),
print >> sys.stderr, self.units.get()
sp = make_tuple(self.sp.get())
ep = make_tuple(self.ep.get())
d = int(self.d.get())
s = int(self.s.get())
self.cleanUp()
# put focus back to the parent window's canvas
self.culebron.canvas.focus_set()
self.destroy()
self.culebron.drag(sp, ep, d, s, self.units.get())
def onCancel(self, event=None):
self.culebron.setGrab(False)
self.cleanUp()
# put focus back to the parent window's canvas
self.culebron.canvas.focus_set()
self.destroy()
def onGrabSp(self):
'''
Grab starting point
'''
self.sp.entry.focus_get()
self.onGrab(self.sp)
def onGrabEp(self):
'''
Grab ending point
'''
self.ep.entry.focus_get()
self.onGrab(self.ep)
def onGrab(self, entry):
'''
Generic grab method.
@param entry: the entry being grabbed
@type entry: Tkinter.Entry
'''
self.culebron.setOnTouchListener(self.onTouchListener)
self.__grabbing = entry
self.culebron.setGrab(True)
def onTouchListener(self, point):
'''
Listens for touch events and draws the corresponding shapes on the Culebron canvas.
If the starting point is being grabbed it draws the touching point via
C{Culebron.drawTouchedPoint()} and if the end point is being grabbed it draws
using C{Culebron.drawDragLine()}.
@param point: the point touched
@type point: tuple
'''
x = point[0]
y = point[1]
value = "(%d,%d)" % (x, y)
self.__grabbing.set(value)
self.onValidate(value)
self.culebron.setGrab(False)
if self.__grabbing == self.sp:
self.__cleanUpSpId()
self.__cleanUpEpId()
self.spX = x
self.spY = y
elif self.__grabbing == self.ep:
self.__cleanUpEpId()
self.epX = x
self.epY = y
if self.spX and self.spY and not self.spId:
self.spId = self.culebron.drawTouchedPoint(self.spX, self.spY)
if self.spX and self.spY and self.epX and self.epY and not self.epId:
self.epId = self.culebron.drawDragLine(self.spX, self.spY, self.epX, self.epY)
self.__grabbing = None
self.culebron.setOnTouchListener(None)
def __cleanUpSpId(self):
if self.spId:
self.culebron.canvas.delete(self.spId)
self.spId = None
def __cleanUpEpId(self):
if self.epId:
self.culebron.canvas.delete(self.epId)
self.epId = None
def cleanUp(self):
self.__cleanUpSpId()
self.__cleanUpEpId()
class ContextMenu(Tkinter.Menu):
# FIXME: should get rid of the nested classes, otherwise it's not possible to create a parent class
# SubMenu for UiScrollableSubMenu
'''
The context menu (popup).
'''
PADDING = ' '
''' Padding used to separate menu entries from border '''
class Separator():
SEPARATOR = 'SEPARATOR'
def __init__(self):
self.description = self.SEPARATOR
class Command():
def __init__(self, description, underline, shortcut, event, command):
self.description = description
self.underline = underline
self.shortcut = shortcut
self.event = event
self.command = command
class UiScrollableSubMenu(Tkinter.Menu):
def __init__(self, menu, description, view, culebron):
# Tkninter.Menu is not extending object, so we can't do this:
# super(ContextMenu, self).__init__(culebron.window, tearoff=False)
Tkinter.Menu.__init__(self, menu, tearoff=False)
self.description = description
self.add_command(label='Fling backward',
command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingBackward))
self.add_command(label='Fling forward',
command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingForward))
self.add_command(label='Fling to beginning',
command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToBeginning))
self.add_command(label='Fling to end',
command=lambda: culebron.executeCommandAndRefresh(view.uiScrollable.flingToEnd))
def __init__(self, culebron, view):
# Tkninter.Menu is not extending object, so we can't do this:
# super(ContextMenu, self).__init__(culebron.window, tearoff=False)
Tkinter.Menu.__init__(self, culebron.window, tearoff=False)
if DEBUG_CONTEXT_MENU:
print >> sys.stderr, "Creating ContextMenu for", view.__smallStr__() if view else "No View"
self.view = view
items = []
if self.view:
_saveViewSnapshotForSelectedView = lambda: culebron.saveViewSnapshot(self.view)
items.append(ContextMenu.Command('Take view snapshot and save to file', 5, 'Ctrl+W', '<Control-W>',
_saveViewSnapshotForSelectedView))
if self.view.uiScrollable:
items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', view, culebron))
else:
parent = self.view.parent
while parent:
if parent.uiScrollable:
# WARNING:
# A bit dangerous, but may work
# If we click ona ListView then the View pased to this ContextMenu is the child,
# perhaps we want to scroll the parent
items.append(ContextMenu.UiScrollableSubMenu(self, 'UiScrollable', parent, culebron))
break
parent = parent.parent
items.append(ContextMenu.Separator())
items.append(ContextMenu.Command('Drag dialog', 0, 'Ctrl+D', '<Control-D>', culebron.showDragDialog))
items.append(ContextMenu.Command('Take snapshot and save to file', 26, 'Ctrl+F', '<Control-F>',
culebron.saveSnapshot))
items.append(ContextMenu.Command('Control Panel', 0, 'Ctrl+K', '<Control-K>', culebron.showControlPanel))
items.append(ContextMenu.Command('Long touch point using PX', 0, 'Ctrl+L', '<Control-L>',
culebron.toggleLongTouchPoint))
items.append(ContextMenu.Command('Long touch View', 0, None, None,
culebron.toggleLongTouchView))
items.append(
ContextMenu.Command('Touch using DIP', 13, 'Ctrl+I', '<Control-I>', culebron.toggleTouchPointDip))
items.append(
ContextMenu.Command('Touch using PX', 12, 'Ctrl+P', '<Control-P>', culebron.toggleTouchPointPx))
items.append(ContextMenu.Command('Generates a Sleep() on output script', 12, 'Ctrl+S', '<Control-S>',
culebron.showSleepDialog))
if culebron.vc is not None:
items.append(ContextMenu.Command('Toggle generating Test Condition', 18, 'Ctrl+T', '<Control-T>',
culebron.toggleGenerateTestCondition))
items.append(ContextMenu.Command('Touch Zones', 6, 'Ctrl+Z', '<Control-Z>', culebron.toggleTargetZones))
items.append(ContextMenu.Command('Generates a startActivity()', 17, 'Ctrl+A', '<Control-A>',
culebron.printStartActivityAtTop))
items.append(ContextMenu.Command('Refresh', 0, 'F5', '<F5>', culebron.refresh))
items.append(ContextMenu.Command('Wake up', 0, None, None, culebron.wake))
items.append(ContextMenu.Separator())
items.append(ContextMenu.Command('Quit', 0, 'Ctrl+Q', '<Control-Q>', culebron.quit))
for item in items:
self.addItem(item)
def addItem(self, item):
if isinstance(item, ContextMenu.Separator):
self.addSeparator()
elif isinstance(item, ContextMenu.Command):
self.addCommand(item)
elif isinstance(item, ContextMenu.UiScrollableSubMenu):
self.addSubMenu(item)
else:
raise RuntimeError("Unsupported item=" + str(item))
def addSeparator(self):
self.add_separator()
def addCommand(self, item):
self.add_command(label=self.PADDING + item.description, underline=item.underline + len(self.PADDING),
command=item.command, accelerator=item.shortcut)
# if item.event:
# # These bindings remain even after the menu has been dismissed, so it seems not a good idea
# #self.bind_all(item.event, item.command)
# pass
def addSubMenu(self, item):
self.add_cascade(label=self.PADDING + item.description, menu=item)
def showPopupMenu(self, event):
try:
self.tk_popup(event.x_root, event.y_root)
finally:
# make sure to release the grab (Tk 8.0a1 only)
# self.grab_release()
pass
class HelpDialog(Tkinter.Toplevel):
def __init__(self, culebron):
self.culebron = culebron
self.parent = culebron.window
Tkinter.Toplevel.__init__(self, self.parent)
# self.transient(self.parent)
self.title("%s: help" % Culebron.APPLICATION_NAME)
self.text = ScrolledText.ScrolledText(self, width=60, height=40)
self.text.insert(Tkinter.INSERT, '''
Special keys
------------
F1: Help
F5: Refresh
Mouse Buttons
-------------
<1>: Touch the underlying View
Commands
--------
Ctrl-A: Generates startActivity() call on output script
Ctrl-D: Drag dialog
Ctrl-F: Take snapshot and save to file
Ctrl-K: Control Panel
Ctrl-L: Long touch point using PX
Ctrl-I: Touch using DIP
Ctrl-P: Touch using PX
Ctrl-Q: Quit
Ctrl-S: Generates a sleep() on output script
Ctrl-T: Toggle generating test condition
Ctrl-V: Verifies the content of the screen dump
Ctrl-Z: Touch zones
''')
self.text.grid(row=1, column=1)
self.buttonBox()
def buttonBox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Tkinter.Frame(self)
w = Tkinter.Button(box, text="Dismiss", width=10, command=self.onDismiss, default=Tkinter.ACTIVE)
w.grid(row=1, column=1, padx=5, pady=5)
self.bind("<Return>", self.onDismiss)
self.bind("<Escape>", self.onDismiss)
box.grid(row=1, column=1)
def onDismiss(self, event=None):
# put focus back to the parent window's canvas
self.culebron.canvas.focus_set()
self.destroy()
class FileDialog():
def __init__(self, culebron, filename):
self.parent = culebron.window
self.filename = filename
self.basename = os.path.basename(self.filename)
self.dirname = os.path.dirname(self.filename)
self.ext = os.path.splitext(self.filename)[1]
self.fileTypes = [('images', self.ext)]
def askSaveAsFilename(self):
return tkFileDialog.asksaveasfilename(parent=self.parent, filetypes=self.fileTypes,
defaultextension=self.ext, initialdir=self.dirname,
initialfile=self.basename)