blob: 820a8b24c97c2a63fe4e87c96070c256b9c3006c [file] [log] [blame]
# -*- coding: utf-8 -*-
'''
Copyright (C) 2012-2015 Diego Torres Milano
Created on Feb 2, 2012
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
'''
__version__ = '11.5.9'
import sys
import warnings
if sys.executable:
if 'monkeyrunner' in sys.executable:
warnings.warn(
'''
You should use a 'python' interpreter, not 'monkeyrunner' for this module
''', RuntimeWarning)
import subprocess
import re
import socket
import os
import types
import time
import signal
import copy
import pickle
import platform
import xml.parsers.expat
import unittest
import StringIO
from com.dtmilano.android.common import _nd, _nh, _ns, obtainPxPy, obtainVxVy,\
obtainVwVh, obtainAdbPath
from com.dtmilano.android.window import Window
from com.dtmilano.android.adb import adbclient
from com.dtmilano.android.uiautomator.uiautomatorhelper import UiAutomatorHelper
DEBUG = False
DEBUG_DEVICE = DEBUG and False
DEBUG_RECEIVED = DEBUG and False
DEBUG_TREE = DEBUG and False
DEBUG_GETATTR = DEBUG and False
DEBUG_CALL = DEBUG and False
DEBUG_COORDS = DEBUG and False
DEBUG_TOUCH = DEBUG and False
DEBUG_STATUSBAR = DEBUG and False
DEBUG_WINDOWS = DEBUG and False
DEBUG_BOUNDS = DEBUG and False
DEBUG_DISTANCE = DEBUG and False
DEBUG_MULTI = DEBUG and False
DEBUG_VIEW = DEBUG and False
DEBUG_VIEW_FACTORY = DEBUG and False
DEBUG_CHANGE_LANGUAGE = DEBUG and False
DEBUG_UI_AUTOMATOR_HELPER = DEBUG and False
DEBUG_NAV_BUTTONS = DEBUG and False
WARNINGS = False
VIEW_SERVER_HOST = 'localhost'
VIEW_SERVER_PORT = 4939
ADB_DEFAULT_PORT = 5555
OFFSET = 25
''' This assumes the smallest touchable view on the screen is approximately 50px x 50px
and touches it at M{(x+OFFSET, y+OFFSET)} '''
USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES = True
''' Use C{AdbClient} to obtain the needed properties. If this is
C{False} then C{adb shell getprop} is used '''
USE_PHYSICAL_DISPLAY_INFO = True
''' Use C{dumpsys display} to obtain display properties. If this is
C{False} then C{USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES} is used '''
SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED = False
''' Skips some classes related with the Action Bar and the PhoneWindow$DecorView in the
coordinates calculation
@see: L{View.getXY()} '''
VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED = False
''' Under some conditions the touch event should be longer [t(DOWN) << t(UP)]. C{True} enables a
workaround to delay the events.'''
# some device properties
VERSION_SDK_PROPERTY = 'ro.build.version.sdk'
VERSION_RELEASE_PROPERTY = 'ro.build.version.release'
# some constants for the attributes
ID_PROPERTY = 'mID'
ID_PROPERTY_UI_AUTOMATOR = 'uniqueId'
TEXT_PROPERTY = 'text:mText'
TEXT_PROPERTY_API_10 = 'mText'
TEXT_PROPERTY_UI_AUTOMATOR = 'text'
WS = u"\xfe" # the whitespace replacement char for TEXT_PROPERTY
TAG_PROPERTY = 'getTag()'
LEFT_PROPERTY = 'layout:mLeft'
LEFT_PROPERTY_API_8 = 'mLeft'
TOP_PROPERTY = 'layout:mTop'
TOP_PROPERTY_API_8 = 'mTop'
WIDTH_PROPERTY = 'layout:getWidth()'
WIDTH_PROPERTY_API_8 = 'getWidth()'
HEIGHT_PROPERTY = 'layout:getHeight()'
HEIGHT_PROPERTY_API_8 = 'getHeight()'
GET_VISIBILITY_PROPERTY = 'getVisibility()'
LAYOUT_TOP_MARGIN_PROPERTY = 'layout:layout_topMargin'
IS_FOCUSED_PROPERTY_UI_AUTOMATOR = 'focused'
IS_FOCUSED_PROPERTY = 'focus:isFocused()'
# visibility
VISIBLE = 0x0
INVISIBLE = 0x4
GONE = 0x8
RegexType = type(re.compile(''))
IP_RE = re.compile('^(\d{1,3}\.){3}\d{1,3}$')
ID_RE = re.compile('id/([^/]*)(/(\d+))?')
class ViewNotFoundException(Exception):
'''
ViewNotFoundException is raised when a View is not found.
'''
def __init__(self, attr, value, root):
if isinstance(value, RegexType):
msg = "Couldn't find View with %s that matches '%s' in tree with root=%s" % (attr, value.pattern, root)
else:
msg = "Couldn't find View with %s='%s' in tree with root=%s" % (attr, value, root)
super(Exception, self).__init__(msg)
class View:
'''
View class
'''
@staticmethod
def factory(arg1, arg2, version=-1, forceviewserveruse=False, windowId=None, uiAutomatorHelper=None):
'''
View factory
@type arg1: ClassType or dict
@type arg2: View instance or AdbClient
'''
if DEBUG_VIEW_FACTORY:
print >> sys.stderr, "View.factory(%s, %s, %s, %s, %s, %s)" % (arg1, arg2, version, forceviewserveruse, windowId, uiAutomatorHelper)
if type(arg1) == types.ClassType:
cls = arg1
attrs = None
else:
cls = None
attrs = arg1
if isinstance(arg2, View):
view = arg2
device = None
else:
device = arg2
view = None
if attrs and attrs.has_key('class'):
clazz = attrs['class']
if DEBUG_VIEW_FACTORY:
print >> sys.stderr, " View.factory: creating View with specific class: %s" % clazz
if clazz == 'android.widget.TextView':
return TextView(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
elif clazz == 'android.widget.EditText':
return EditText(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
elif clazz == 'android.widget.ListView':
return ListView(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
else:
return View(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
elif cls:
if view:
return cls.__copy(view)
else:
return cls(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
elif view:
return copy.copy(view)
else:
if DEBUG_VIEW_FACTORY:
print >> sys.stderr, " View.factory: creating generic View"
return View(attrs, device, version, forceviewserveruse, windowId, uiAutomatorHelper)
@classmethod
def __copy(cls, view):
'''
Copy constructor
'''
return cls(view.map, view.device, view.version, view.forceviewserveruse, view.windowId, view.uiAutomatorHelper)
def __init__(self, _map, device, version=-1, forceviewserveruse=False, windowId=None, uiAutomatorHelper=None):
'''
Constructor
@type _map: map
@param _map: the map containing the (attribute, value) pairs
@type device: AdbClient
@param device: the device containing this View
@type version: int
@param version: the Android SDK version number of the platform where this View belongs. If
this is C{-1} then the Android SDK version will be obtained in this
constructor.
@type forceviewserveruse: boolean
@param forceviewserveruse: Force the use of C{ViewServer} even if the conditions were given
to use C{UiAutomator}.
@type uiAutomatorHelper: UiAutomatorHelper
@:param uiAutomatorHelper: The UiAutomatorHelper if available
'''
if DEBUG_VIEW:
print >> sys.stderr, "View.__init__(%s, %s, %s, %s)" % ("map" if _map is not None else None, device, version, forceviewserveruse)
if _map:
print >> sys.stderr, " map:", type(_map)
for attr, val in _map.iteritems():
if len(val) > 50:
val = val[:50] + "..."
print >> sys.stderr, " %s=%s" % (attr, val)
self.map = _map
''' The map that contains the C{attr},C{value} pairs '''
self.device = device
''' The AdbClient '''
self.children = []
''' The children of this View '''
self.parent = None
''' The parent of this View '''
self.windows = {}
self.currentFocus = None
''' The current focus '''
self.windowId = windowId
''' The window this view resides '''
self.build = {}
''' Build properties '''
self.version = version
''' API version number '''
self.forceviewserveruse = forceviewserveruse
''' Force ViewServer use '''
self.uiScrollable = None
''' If this is a scrollable View this keeps the L{UiScrollable} object '''
self.target = False
''' Is this a touch target zone '''
self.uiAutomatorHelper = uiAutomatorHelper
''' The UiAutomatorHelper '''
if version != -1:
self.build[VERSION_SDK_PROPERTY] = version
else:
try:
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
self.build[VERSION_SDK_PROPERTY] = int(device.getProperty(VERSION_SDK_PROPERTY))
else:
self.build[VERSION_SDK_PROPERTY] = int(device.shell('getprop ' + VERSION_SDK_PROPERTY)[:-2])
except:
self.build[VERSION_SDK_PROPERTY] = -1
version = self.build[VERSION_SDK_PROPERTY]
self.useUiAutomator = (version >= 16) and not forceviewserveruse
''' Whether to use UIAutomator or ViewServer '''
self.idProperty = None
''' The id property depending on the View attribute format '''
self.textProperty = None
''' The text property depending on the View attribute format '''
self.tagProperty = None
''' The tag property depending on the View attribute format '''
self.leftProperty = None
''' The left property depending on the View attribute format '''
self.topProperty = None
''' The top property depending on the View attribute format '''
self.widthProperty = None
''' The width property depending on the View attribute format '''
self.heightProperty = None
''' The height property depending on the View attribute format '''
self.isFocusedProperty = None
''' The focused property depending on the View attribute format '''
if version >= 16 and self.useUiAutomator:
self.idProperty = ID_PROPERTY_UI_AUTOMATOR
self.textProperty = TEXT_PROPERTY_UI_AUTOMATOR
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY_UI_AUTOMATOR
elif version > 10 and (version < 16 or self.useUiAutomator):
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY
elif version == 10:
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY_API_10
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY
elif version >= 7 and version < 10:
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY_API_10
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY_API_8
self.topProperty = TOP_PROPERTY_API_8
self.widthProperty = WIDTH_PROPERTY_API_8
self.heightProperty = HEIGHT_PROPERTY_API_8
self.isFocusedProperty = IS_FOCUSED_PROPERTY
elif version > 0 and version < 7:
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY_API_10
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY
elif version == -1:
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY
else:
self.idProperty = ID_PROPERTY
self.textProperty = TEXT_PROPERTY
self.tagProperty = TAG_PROPERTY
self.leftProperty = LEFT_PROPERTY
self.topProperty = TOP_PROPERTY
self.widthProperty = WIDTH_PROPERTY
self.heightProperty = HEIGHT_PROPERTY
self.isFocusedProperty = IS_FOCUSED_PROPERTY
try:
if self.isScrollable():
self.uiScrollable = UiScrollable(self)
except AttributeError:
pass
def __getitem__(self, key):
return self.map[key]
def __getattr__(self, name):
if DEBUG_GETATTR:
print >>sys.stderr, "__getattr__(%s) version: %d" % (name, self.build[VERSION_SDK_PROPERTY])
# NOTE:
# I should try to see if 'name' is a defined method
# but it seems that if I call locals() here an infinite loop is entered
if self.map.has_key(name):
r = self.map[name]
elif self.map.has_key(name + '()'):
# the method names are stored in the map with their trailing '()'
r = self.map[name + '()']
elif name.count("_") > 0:
mangledList = self.allPossibleNamesWithColon(name)
mangledName = self.intersection(mangledList, self.map.keys())
if len(mangledName) > 0 and self.map.has_key(mangledName[0]):
r = self.map[mangledName[0]]
else:
# Default behavior
raise AttributeError, name
elif name.startswith('is'):
# try removing 'is' prefix
if DEBUG_GETATTR:
print >> sys.stderr, " __getattr__: trying without 'is' prefix"
suffix = name[2:].lower()
if self.map.has_key(suffix):
r = self.map[suffix]
else:
# Default behavior
raise AttributeError, name
elif name.startswith('get'):
# try removing 'get' prefix
if DEBUG_GETATTR:
print >> sys.stderr, " __getattr__: trying without 'get' prefix"
suffix = name[3:].lower()
if self.map.has_key(suffix):
r = self.map[suffix]
else:
# Default behavior
raise AttributeError, name
elif name == 'getResourceId':
if DEBUG_GETATTR:
print >> sys.stderr, " __getattr__: getResourceId"
if self.map.has_key('resource-id'):
r = self.map['resource-id']
else:
# Default behavior
raise AttributeError, name
else:
# Default behavior
raise AttributeError, name
# if the method name starts with 'is' let's assume its return value is boolean
# if name[:2] == 'is':
# r = True if r == 'true' else False
if r == 'true':
r = True
elif r == 'false':
r = False
# this should not cached in some way
def innerMethod():
if DEBUG_GETATTR:
print >>sys.stderr, "innerMethod: %s returning %s" % (innerMethod.__name__, r)
return r
innerMethod.__name__ = name
# this should work, but then there's problems with the arguments of innerMethod
# even if innerMethod(self) is added
#setattr(View, innerMethod.__name__, innerMethod)
#setattr(self, innerMethod.__name__, innerMethod)
return innerMethod
def __call__(self, *args, **kwargs):
if DEBUG_CALL:
print >>sys.stderr, "__call__(%s)" % (args if args else None)
def getClass(self):
'''
Gets the L{View} class
@return: the L{View} class or C{None} if not defined
'''
try:
return self.map['class']
except:
return None
def getId(self):
'''
Gets the L{View} Id
@return: the L{View} C{Id} or C{None} if not defined
@see: L{getUniqueId()}
'''
try:
return self.map['resource-id']
except:
pass
try:
return self.map[self.idProperty]
except:
return None
def getContentDescription(self):
'''
Gets the content description.
'''
try:
return self.map['content-desc']
except:
return None
def getTag(self):
'''
Gets the tag.
'''
try:
return self.map[self.tagProperty]
except:
return None
def getParent(self):
'''
Gets the parent.
'''
return self.parent
def getChildren(self):
'''
Gets the children of this L{View}.
'''
return self.children
def getText(self):
'''
Gets the text attribute.
@return: the text attribute or C{None} if not defined
'''
try:
return self.map[self.textProperty]
except Exception:
return None
def getHeight(self):
'''
Gets the height.
'''
if self.useUiAutomator:
return self.map['bounds'][1][1] - self.map['bounds'][0][1]
else:
try:
return int(self.map[self.heightProperty])
except:
return 0
def getWidth(self):
'''
Gets the width.
'''
if self.useUiAutomator:
return self.map['bounds'][1][0] - self.map['bounds'][0][0]
else:
try:
return int(self.map[self.widthProperty])
except:
return 0
def getUniqueId(self):
'''
Gets the unique Id of this View.
@see: L{ViewClient.__splitAttrs()} for a discussion on B{Unique Ids}
'''
try:
return self.map['uniqueId']
except:
return None
def getVisibility(self):
'''
Gets the View visibility
'''
try:
if self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
return VISIBLE
elif self.map[GET_VISIBILITY_PROPERTY] == 'INVISIBLE':
return INVISIBLE
elif self.map[GET_VISIBILITY_PROPERTY] == 'GONE':
return GONE
else:
return -2
except:
return -1
def getX(self):
'''
Gets the View X coordinate
'''
return self.getXY()[0]
def __getX(self):
'''
Gets the View X coordinate
'''
if DEBUG_COORDS:
print >>sys.stderr, "getX(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
x = 0
if self.useUiAutomator:
x = self.map['bounds'][0][0]
else:
try:
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
_x = int(self.map[self.leftProperty])
if DEBUG_COORDS: print >>sys.stderr, " getX: VISIBLE adding %d" % _x
x += _x
except:
warnings.warn("View %s has no '%s' property" % (self.getId(), self.leftProperty))
if DEBUG_COORDS: print >>sys.stderr, " getX: returning %d" % (x)
return x
def getY(self):
'''
Gets the View Y coordinate
'''
return self.getXY()[1]
def __getY(self):
'''
Gets the View Y coordinate
'''
if DEBUG_COORDS:
print >>sys.stderr, "getY(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
y = 0
if self.useUiAutomator:
y = self.map['bounds'][0][1]
else:
try:
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
_y = int(self.map[self.topProperty])
if DEBUG_COORDS: print >>sys.stderr, " getY: VISIBLE adding %d" % _y
y += _y
except:
warnings.warn("View %s has no '%s' property" % (self.getId(), self.topProperty))
if DEBUG_COORDS: print >>sys.stderr, " getY: returning %d" % (y)
return y
def getXY(self, debug=False):
'''
Returns the I{screen} coordinates of this C{View}.
WARNING: Don't call self.getX() or self.getY() inside this method
or it will enter an infinite loop
@return: The I{screen} coordinates of this C{View}
'''
if DEBUG_COORDS or debug:
try:
_id = self.getId()
except:
_id = "NO_ID"
print >> sys.stderr, "getXY(%s %s ## %s)" % (self.getClass(), _id, self.getUniqueId())
x = self.__getX()
y = self.__getY()
if self.useUiAutomator:
return (x, y)
parent = self.parent
if DEBUG_COORDS: print >> sys.stderr, " getXY: x=%s y=%s parent=%s" % (x, y, parent.getUniqueId() if parent else "None")
hx = 0
''' Hierarchy accumulated X '''
hy = 0
''' Hierarchy accumulated Y '''
if DEBUG_COORDS: print >> sys.stderr, " getXY: not using UiAutomator, calculating parent coordinates"
while parent != None:
if DEBUG_COORDS: print >> sys.stderr, " getXY: parent: %s %s <<<<" % (parent.getClass(), parent.getId())
if SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED:
if parent.getClass() in [ 'com.android.internal.widget.ActionBarView',
'com.android.internal.widget.ActionBarContextView',
'com.android.internal.view.menu.ActionMenuView',
'com.android.internal.policy.impl.PhoneWindow$DecorView' ]:
if DEBUG_COORDS: print >> sys.stderr, " getXY: skipping %s %s (%d,%d)" % (parent.getClass(), parent.getId(), parent.__getX(), parent.__getY())
parent = parent.parent
continue
if DEBUG_COORDS: print >> sys.stderr, " getXY: parent=%s x=%d hx=%d y=%d hy=%d" % (parent.getId(), x, hx, y, hy)
hx += parent.__getX()
hy += parent.__getY()
parent = parent.parent
(wvx, wvy) = self.__dumpWindowsInformation(debug=debug)
if DEBUG_COORDS or debug:
print >>sys.stderr, " getXY: wv=(%d, %d) (windows information)" % (wvx, wvy)
try:
if self.windowId:
fw = self.windows[self.windowId]
else:
fw = self.windows[self.currentFocus]
if DEBUG_STATUSBAR:
print >> sys.stderr, " getXY: focused window=", fw
print >> sys.stderr, " getXY: deciding whether to consider statusbar offset because current focused windows is at", (fw.wvx, fw.wvy), "parent", (fw.px, fw.py)
except KeyError:
fw = None
(sbw, sbh) = self.__obtainStatusBarDimensionsIfVisible()
if DEBUG_COORDS or debug:
print >>sys.stderr, " getXY: sb=(%d, %d) (statusbar dimensions)" % (sbw, sbh)
statusBarOffset = 0
pwx = 0
pwy = 0
if fw:
if DEBUG_COORDS:
print >>sys.stderr, " getXY: focused window=", fw, "sb=", (sbw, sbh)
if fw.wvy <= sbh: # it's very unlikely that fw.wvy < sbh, that is a window over the statusbar
if DEBUG_STATUSBAR: print >>sys.stderr, " getXY: yes, considering offset=", sbh
statusBarOffset = sbh
else:
if DEBUG_STATUSBAR: print >>sys.stderr, " getXY: no, ignoring statusbar offset fw.wvy=", fw.wvy, ">", sbh
if fw.py == fw.wvy:
if DEBUG_STATUSBAR: print >>sys.stderr, " getXY: but wait, fw.py == fw.wvy so we are adjusting by ", (fw.px, fw.py)
pwx = fw.px
pwy = fw.py
else:
if DEBUG_STATUSBAR: print >>sys.stderr, " getXY: fw.py=%d <= fw.wvy=%d, no adjustment" % (fw.py, fw.wvy)
if DEBUG_COORDS or DEBUG_STATUSBAR or debug:
print >>sys.stderr, " getXY: returning (%d, %d) ***" % (x+hx+wvx+pwx, y+hy+wvy-statusBarOffset+pwy)
print >>sys.stderr, " x=%d+%d+%d+%d" % (x,hx,wvx,pwx)
print >>sys.stderr, " y=%d+%d+%d-%d+%d" % (y,hy,wvy,statusBarOffset,pwy)
return (x+hx+wvx+pwx, y+hy+wvy-statusBarOffset+pwy)
def getCoords(self):
'''
Gets the coords of the View
@return: A tuple containing the View's coordinates ((L, T), (R, B))
'''
if DEBUG_COORDS:
print >>sys.stderr, "getCoords(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
(x, y) = self.getXY();
w = self.getWidth()
h = self.getHeight()
return ((x, y), (x+w, y+h))
def getPositionAndSize(self):
'''
Gets the position and size (X,Y, W, H)
@return: A tuple containing the View's coordinates (X, Y, W, H)
'''
(x, y) = self.getXY();
w = self.getWidth()
h = self.getHeight()
return (x, y, w, h)
def getBounds(self):
'''
Gets the View bounds
'''
if 'bounds' in self.map:
return self.map['bounds']
else:
return self.getCoords()
def getCenter(self):
'''
Gets the center coords of the View
@author: U{Dean Morin <https://github.com/deanmorin>}
'''
(left, top), (right, bottom) = self.getCoords()
x = left + (right - left) / 2
y = top + (bottom - top) / 2
return (x, y)
def __obtainStatusBarDimensionsIfVisible(self):
sbw = 0
sbh = 0
for winId in self.windows:
w = self.windows[winId]
if DEBUG_COORDS: print >> sys.stderr, " __obtainStatusBarDimensionsIfVisible: w=", w, " w.activity=", w.activity, "%%%"
if w.activity == 'StatusBar':
if w.wvy == 0 and w.visibility == 0:
if DEBUG_COORDS: print >> sys.stderr, " __obtainStatusBarDimensionsIfVisible: statusBar=", (w.wvw, w.wvh)
sbw = w.wvw
sbh = w.wvh
break
return (sbw, sbh)
def __obtainVxVy(self, m):
return obtainVxVy(m)
def __obtainVwVh(self, m):
return obtainVwVh(m)
def __obtainPxPy(self, m):
return obtainPxPy(m)
def __dumpWindowsInformation(self, debug=False):
self.windows = {}
self.currentFocus = None
dww = self.device.shell('dumpsys window windows')
if DEBUG_WINDOWS or debug: print >> sys.stderr, dww
lines = dww.splitlines()
widRE = re.compile('^ *Window #%s Window\{%s (u\d+ )?%s?.*\}:' %
(_nd('num'), _nh('winId'), _ns('activity', greedy=True)))
currentFocusRE = re.compile('^ mCurrentFocus=Window\{%s .*' % _nh('winId'))
viewVisibilityRE = re.compile(' mViewVisibility=0x%s ' % _nh('visibility'))
# This is for 4.0.4 API-15
containingFrameRE = re.compile('^ *mContainingFrame=\[%s,%s\]\[%s,%s\] mParentFrame=\[%s,%s\]\[%s,%s\]' %
(_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
contentFrameRE = re.compile('^ *mContentFrame=\[%s,%s\]\[%s,%s\] mVisibleFrame=\[%s,%s\]\[%s,%s\]' %
(_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
# This is for 4.1 API-16
framesRE = re.compile('^ *Frames: containing=\[%s,%s\]\[%s,%s\] parent=\[%s,%s\]\[%s,%s\]' %
(_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
contentRE = re.compile('^ *content=\[%s,%s\]\[%s,%s\] visible=\[%s,%s\]\[%s,%s\]' %
(_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
policyVisibilityRE = re.compile('mPolicyVisibility=%s ' % _ns('policyVisibility', greedy=True))
for l in range(len(lines)):
m = widRE.search(lines[l])
if m:
num = int(m.group('num'))
winId = m.group('winId')
activity = m.group('activity')
wvx = 0
wvy = 0
wvw = 0
wvh = 0
px = 0
py = 0
visibility = -1
policyVisibility = 0x0
for l2 in range(l+1, len(lines)):
m = widRE.search(lines[l2])
if m:
l += (l2-1)
break
m = viewVisibilityRE.search(lines[l2])
if m:
visibility = int(m.group('visibility'))
if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: visibility=", visibility
if self.build[VERSION_SDK_PROPERTY] >= 17:
m = framesRE.search(lines[l2])
if m:
px, py = obtainPxPy(m)
m = contentRE.search(lines[l2+2])
if m:
wvx, wvy = obtainVxVy(m)
wvw, wvh = obtainVwVh(m)
elif self.build[VERSION_SDK_PROPERTY] >= 16:
m = framesRE.search(lines[l2])
if m:
px, py = self.__obtainPxPy(m)
m = contentRE.search(lines[l2+1])
if m:
# FIXME: the information provided by 'dumpsys window windows' in 4.2.1 (API 16)
# when there's a system dialog may not be correct and causes the View coordinates
# be offset by this amount, see
# https://github.com/dtmilano/AndroidViewClient/issues/29
wvx, wvy = self.__obtainVxVy(m)
wvw, wvh = self.__obtainVwVh(m)
elif self.build[VERSION_SDK_PROPERTY] == 15:
m = containingFrameRE.search(lines[l2])
if m:
px, py = self.__obtainPxPy(m)
m = contentFrameRE.search(lines[l2+1])
if m:
wvx, wvy = self.__obtainVxVy(m)
wvw, wvh = self.__obtainVwVh(m)
elif self.build[VERSION_SDK_PROPERTY] == 10:
m = containingFrameRE.search(lines[l2])
if m:
px, py = self.__obtainPxPy(m)
m = contentFrameRE.search(lines[l2+1])
if m:
wvx, wvy = self.__obtainVxVy(m)
wvw, wvh = self.__obtainVwVh(m)
else:
warnings.warn("Unsupported Android version %d" % self.build[VERSION_SDK_PROPERTY])
#print >> sys.stderr, "Searching policyVisibility in", lines[l2]
m = policyVisibilityRE.search(lines[l2])
if m:
policyVisibility = 0x0 if m.group('policyVisibility') == 'true' else 0x8
self.windows[winId] = Window(num, winId, activity, wvx, wvy, wvw, wvh, px, py, visibility + policyVisibility)
else:
m = currentFocusRE.search(lines[l])
if m:
self.currentFocus = m.group('winId')
if self.windowId and self.windowId in self.windows and self.windows[self.windowId].visibility == 0:
w = self.windows[self.windowId]
return (w.wvx, w.wvy)
elif self.currentFocus in self.windows and self.windows[self.currentFocus].visibility == 0:
if DEBUG_COORDS or debug:
print >> sys.stderr, "__dumpWindowsInformation: focus=", self.currentFocus
print >> sys.stderr, "__dumpWindowsInformation:", self.windows[self.currentFocus]
w = self.windows[self.currentFocus]
return (w.wvx, w.wvy)
else:
if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: (0,0)"
return (0,0)
def touch(self, eventType=adbclient.DOWN_AND_UP, deltaX=0, deltaY=0):
'''
Touches the center of this C{View}. The touch can be displaced from the center by
using C{deltaX} and C{deltaY} values.
@param eventType: The event type
@type eventType: L{adbclient.DOWN}, L{adbclient.UP} or L{adbclient.DOWN_AND_UP}
@param deltaX: Displacement from center (X axis)
@type deltaX: int
@param deltaY: Displacement from center (Y axis)
@type deltaY: int
'''
(x, y) = self.getCenter()
if deltaX:
x += deltaX
if deltaY:
y += deltaY
if DEBUG_TOUCH:
print >>sys.stderr, "should touch @ (%d, %d)" % (x, y)
if VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED and eventType == adbclient.DOWN_AND_UP:
if WARNINGS:
print >> sys.stderr, "ViewClient: touch workaround enabled"
self.device.touch(x, y, eventType=adbclient.DOWN)
time.sleep(50/1000.0)
self.device.touch(x+10, y+10, eventType=adbclient.UP)
else:
if self.uiAutomatorHelper:
selector = self.obtainSelectorForView()
if selector:
try:
oid = self.uiAutomatorHelper.findObject(bySelector=selector)
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "oid=", oid
print >> sys.stderr, "ignoring click delta to click View as UiObject"
oid.click();
except RuntimeError as e:
print >> sys.stderr, e.message
print >> sys.stderr, "UiObject click failed, using co-ordinates"
self.uiAutomatorHelper.click(x=x, y=y)
else:
# FIXME:
# The View has no CD, TEXT or ID so we cannot use it in a selector to findObject()
# We should try content description, text, and perhaps other properties before surrendering.
# For now, tet's fall back to click(x, y)
self.uiAutomatorHelper.click(x=x, y=y)
else:
self.device.touch(x, y, eventType=eventType)
def escapeSelectorChars(self, selector):
return selector.replace('@', '\\@').replace(',', '\\,')
def obtainSelectorForView(self):
selector = ''
if self.getContentDescription():
selector += 'desc@' + self.escapeSelectorChars(self.getContentDescription())
if self.getText():
if selector:
selector += ','
selector += 'text@' + self.escapeSelectorChars(self.getText())
if self.getId():
if selector:
selector += ','
selector += 'res@' + self.escapeSelectorChars(self.getId())
return selector
def longTouch(self, duration=2000):
'''
Long touches this C{View}
@param duration: duration in ms
'''
(x, y) = self.getCenter()
if self.uiAutomatorHelper:
self.uiAutomatorHelper.swipe(startX=x, startY=y, endX=x, endY=y, steps=200)
else:
# FIXME: get orientation
self.device.longTouch(x, y, duration, orientation=-1)
def allPossibleNamesWithColon(self, name):
l = []
for _ in range(name.count("_")):
name = name.replace("_", ":", 1)
l.append(name)
return l
def intersection(self, l1, l2):
return list(set(l1) & set(l2))
def containsPoint(self, (x, y)):
(X, Y, W, H) = self.getPositionAndSize()
return (((x >= X) and (x <= (X+W)) and ((y >= Y) and (y <= (Y+H)))))
def add(self, child):
'''
Adds a child
@type child: View
@param child: The child to add
'''
child.parent = self
self.children.append(child)
def isClickable(self):
return self.__getattr__('isClickable')()
def isFocused(self):
'''
Gets the focused value
@return: the focused value. If the property cannot be found returns C{False}
'''
try:
return True if self.map[self.isFocusedProperty].lower() == 'true' else False
except Exception:
return False
def variableNameFromId(self):
_id = self.getId()
if _id:
var = _id.replace('.', '_').replace(':', '___').replace('/', '_')
else:
_id = self.getUniqueId()
m = ID_RE.match(_id)
if m:
var = m.group(1)
if m.group(3):
var += m.group(3)
if re.match('^\d', var):
var = 'id_' + var
return var
def setTarget(self, target):
self.target = target
def isTarget(self):
return self.target
def writeImageToFile(self, filename, _format="PNG"):
'''
Write the View image to the specified filename in the specified format.
@type filename: str
@param filename: Absolute path and optional filename receiving the image. If this points to
a directory, then the filename is determined by this View unique ID and
format extension.
@type _format: str
@param _format: Image format (default format is PNG)
'''
filename = self.device.substituteDeviceTemplate(filename)
if not os.path.isabs(filename):
raise ValueError("writeImageToFile expects an absolute path (fielname='%s')" % filename)
if os.path.isdir(filename):
filename = os.path.join(filename, self.variableNameFromId() + '.' + _format.lower())
if DEBUG:
print >> sys.stderr, "writeImageToFile: saving image to '%s' in %s format" % (filename, _format)
#self.device.takeSnapshot().getSubImage(self.getPositionAndSize()).writeToFile(filename, _format)
# crop:
# im.crop(box) ⇒ image
# Returns a copy of a rectangular region from the current image.
# The box is a 4-tuple defining the left, upper, right, and lower pixel coordinate.
((l, t), (r, b)) = self.getCoords()
box = (l, t, r, b)
if DEBUG:
print >> sys.stderr, "writeImageToFile: cropping", box, " reconnect=", self.device.reconnect
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Taking screenshot using UiAutomatorHelper"
received = self.uiAutomatorHelper.takeScreenshot()
stream = StringIO.StringIO(received)
try:
from PIL import Image
image = Image.open(stream)
except ImportError as ex:
# FIXME: this method should be global
self.pilNotInstalledWarning()
sys.exit(1)
except IOError, ex:
print >> sys.stderr, ex
print repr(stream)
sys.exit(1)
else:
image = self.device.takeSnapshot(reconnect=self.device.reconnect)
image.crop(box).save(filename, _format)
def __smallStr__(self):
__str = unicode("View[", 'utf-8', 'replace')
if "class" in self.map:
__str += " class=" + self.map['class']
__str += " id=%s" % self.getId()
__str += " ] parent="
if self.parent and "class" in self.parent.map:
__str += "%s" % self.parent.map["class"]
else:
__str += "None"
return __str
def __tinyStr__(self):
__str = unicode("View[", 'utf-8', 'replace')
if "class" in self.map:
__str += " class=" + re.sub('.*\.', '', self.map['class'])
__str += " id=%s" % self.getId()
__str += " ]"
return __str
def __microStr__(self):
__str = unicode('', 'utf-8', 'replace')
if "class" in self.map:
__str += re.sub('.*\.', '', self.map['class'])
_id = self.getId().replace('id/no_id/', '-')
__str += _id
((L, T), (R, B)) = self.getCoords()
__str += '@%04d%04d%04d%04d' % (L, T, R, B)
__str += ''
return __str
def __str__(self):
__str = unicode("View[", 'utf-8', 'replace')
if "class" in self.map:
__str += " class=" + self.map["class"].__str__() + " "
for a in self.map:
__str += a + "="
# decode() works only on python's 8-bit strings
if isinstance(self.map[a], unicode):
__str += self.map[a]
else:
__str += unicode(str(self.map[a]), 'utf-8', errors='replace')
__str += " "
__str += "] parent="
if self.parent:
if "class" in self.parent.map:
__str += "%s" % self.parent.map["class"]
else:
__str += self.parent.getId().__str__()
else:
__str += "None"
return __str
class TextView(View):
'''
TextView class.
'''
pass
class EditText(TextView):
'''
EditText class.
'''
def type(self, text, alreadyTouched=False):
if not text:
return
if not alreadyTouched:
self.touch()
time.sleep(0.5)
self.device.type(text)
time.sleep(0.5)
def setText(self, text):
"""
This function makes sure that any previously entered text is deleted before
setting the value of the field.
"""
if self.text() == text:
return
self.touch()
guardrail = 0
maxSize = len(self.text()) + 1
while maxSize > guardrail:
guardrail += 1
self.device.press('KEYCODE_DEL', adbclient.DOWN_AND_UP)
self.device.press('KEYCODE_FORWARD_DEL', adbclient.DOWN_AND_UP)
self.type(text, alreadyTouched=True)
def backspace(self):
self.touch()
time.sleep(1)
self.device.press('KEYCODE_DEL', adbclient.DOWN_AND_UP)
class UiDevice():
'''
Provides access to state information about the device. You can also use this class to simulate
user actions on the device, such as pressing the d-pad or pressing the Home and Menu buttons.
'''
def __init__(self, vc):
self.vc = vc
self.device = self.vc.device
def openNotification(self):
'''
Opens the notification shade.
'''
# the tablet has a different Notification/Quick Settings bar depending on x
w13 = self.device.display['width'] / 3
s = (w13, 0)
e = (w13, self.device.display['height']/2)
self.device.drag(s, e, 500, 20, -1)
self.vc.sleep(1)
self.vc.dump(-1)
def openQuickSettings(self):
'''
Opens the Quick Settings shade.
'''
# the tablet has a different Notification/Quick Settings bar depending on x
w23 = 2 * self.device.display['width'] / 3
s = (w23, 0)
e = (w23, self.device.display['height']/2)
self.device.drag(s, e, 500, 20, -1)
self.vc.sleep(1)
if self.vc.getSdkVersion() >= 20:
self.device.drag(s, e, 500, 20, -1)
self.vc.sleep(1)
self.vc.dump(-1)
def openQuickSettingsSettings(self):
'''
Opens the Quick Settings shade and then tries to open Settings from there.
'''
STATUS_BAR_SETTINGS_SETTINGS_BUTTON = [
u"Settings", u"Cài đặt", u"Instellingen", u"Կարգավորումներ", u"设置", u"Nastavitve", u"සැකසීම්", u"Ayarlar",
u"Setelan", u"Настройки", u"تنظیمات", u"Mga Setting", u"Тохиргоо", u"Configuració", u"Setări", u"Налады",
u"Einstellungen", u"პარამეტრები", u"सेटिङहरू", u"Կարգավորումներ", u"Nustatymai", u"Beállítások", u"設定",
u"सेटिंग", u"Настройки", u"Inställningar", u"設定", u"ການຕັ້ງຄ່າ", u"Configurações", u"Tetapan", u"설정",
u"ការ​កំណត់", u"Ajustes", u"הגדרות", u"Ustawienia", u"Nastavení", u"Ρυθμίσεις", u"Тохиргоо", u"Ayarlar",
u"Indstillinger", u"Налаштування", u"Mipangilio", u"Izilungiselelo", u"設定", u"Nastavenia", u"Paramètres",
u"ቅንብሮች", u"การตั้งค่า", u"Seaded", u"Iestatījumi", u"Innstillinger", u"Подешавања", u"الإعدادات", u"සැකසීම්",
u"Definições", u"Configuración", u"პარამეტრები", u"Postavke", u"Ayarlar", u"Impostazioni", u"Asetukset",
u"Instellings", u"Seaded", u"ការ​កំណត់", u"सेटिङहरू", u"Tetapan"
]
self.openQuickSettings()
# this works on API >= 20
found = False
for s in STATUS_BAR_SETTINGS_SETTINGS_BUTTON:
if DEBUG:
print >> sys.stderr, u"finding view with cd=", type(s)
view = self.vc.findViewWithContentDescription(u'''{0}'''.format(s))
if view:
found = True
view.touch()
break
if not found:
# for previous APIs, let's find the text
for s in STATUS_BAR_SETTINGS_SETTINGS_BUTTON:
if DEBUG:
print >> sys.stderr, "s=", type(s)
try:
print >> sys.stderr, "finding view with text=", u'''{0}'''.format(s)
except:
pass
view = self.vc.findViewWithText(s)
if view:
found = True
view.touch()
break
if not found:
raise ViewNotFoundException("content-description", "'Settings' or text 'Settings'", "ROOT")
self.vc.sleep(1)
self.vc.dump(window=-1)
def changeLanguage(self, languageTo):
LANGUAGE_SETTINGS = {
"en": u"Language & input",
"af": u"Taal en invoer",
"am": u"ቋንቋ እና ግቤት",
"ar": u"اللغة والإدخال",
"az": u"Dil və daxiletmə",
"az-rAZ": u"Dil və daxiletmə",
"be": u"Мова і ўвод",
"bg": u"Език и въвеждане",
"ca": u"Idioma i introducció de text",
"cs": u"Jazyk a zadávání",
"da": u"Sprog og input",
"de": u"Sprache & Eingabe",
"el": u"Γλώσσα και εισαγωγή",
"en-rGB": u"Language & input",
"en-rIN": u"Language & input",
"es": u"Idioma e introducción de texto",
"es-rUS": u"Teclado e idioma",
"et": u"Keeled ja sisestamine",
"et-rEE": u"Keeled ja sisestamine",
"fa": u"زبان و ورود اطلاعات",
"fi": u"Kieli ja syöttötapa",
"fr": u"Langue et saisie",
"fr-rCA": u"Langue et saisie",
"hi": u"भाषा और अक्षर",
"hr": u"Jezik i ulaz",
"hu": u"Nyelv és bevitel",
"hy": u"Լեզվի & ներմուծում",
"hy-rAM": u"Լեզու և ներմուծում",
"in": u"Bahasa & masukan",
"it": u"Lingua e immissione",
"iw": u"שפה וקלט",
"ja": u"言語と入力",
"ka": u"ენისა და შეყვანის პარამეტრები",
"ka-rGE": u"ენისა და შეყვანის პარამეტრები",
"km": u"ភាសា & ការ​បញ្ចូល",
"km-rKH": u"ភាសា & ការ​បញ្ចូល",
"ko": u"언어 및 키보드",
"lo": u"ພາສາ & ການປ້ອນຂໍ້ມູນ",
"lo-rLA": u"ພາສາ & ການປ້ອນຂໍ້ມູນ",
"lt": u"Kalba ir įvestis",
"lv": u"Valodas ievade",
"mn": u"Хэл & оруулах",
"mn-rMN": u"Хэл & оруулах",
"ms": u"Bahasa & input",
"ms-rMY": u"Bahasa & input",
"nb": u"Språk og inndata",
"ne": u"भाषा र इनपुट",
"ne-rNP": u"भाषा र इनपुट",
"nl": u"Taal en invoer",
"pl": u"Język, klawiatura, głos",
"pt": u"Idioma e entrada",
"pt-rPT": u"Idioma e entrada",
"ro": u"Limbă și introducere de text",
"ru": u"Язык и ввод",
"si": u"භාෂාව සහ ආදානය",
"si-rLK": u"භාෂාව සහ ආදානය",
"sk": u"Jazyk & vstup",
"sl": u"Jezik in vnos",
"sr": u"Језик и унос",
"sv": u"Språk och inmatning",
"sw": u"Lugha, Kibodi na Sauti",
"th": u"ภาษาและการป้อนข้อมูล",
"tl": u"Wika at input",
"tr": u"Dil ve giriş",
"uk": u"Мова та введення",
"vi": u"Ngôn ngữ & phương thức nhập",
"zh-rCN": u"语言和输入法",
"zh-rHK": u"語言與輸入裝置",
"zh-rTW": u"語言與輸入設定",
"zu": u"Ulimi & ukufakwa",
}
PHONE_LANGUAGE = {
"en": u"Language",
"af": u"Taal",
"am": u"ቋንቋ",
"ar": u"اللغة",
"az": u"Dil",
"az-rAZ": u"Dil",
"be": u"Мова",
"bg": u"Език",
"ca": u"Idioma",
"cs": u"Jazyk",
"da": u"Sprog",
"de": u"Sprache",
"el": u"Γλώσσα",
"en-rGB": u"Language",
"en-rIN": u"Language",
"es": u"Idioma",
"es-rUS": u"Idioma",
"et": u"Keel",
"et-rEE": u"Keel",
"fa": u"زبان",
"fi": u"Kieli",
"fr": u"Langue",
"fr-rCA": u"Langue",
"hi": u"भाषा",
"hr": u"Jezik",
"hu": u"Nyelv",
"hy": u"Lեզուն",
"hy-rAM": u"Lեզուն",
"in": u"Bahasa",
"it": u"Lingua",
"iw": u"שפה",
"ja": u"言語",
"ka": u"ენა",
"ka-rGE": u"ენა",
"km": u"ភាសា",
"km-rKH": u"ភាសា",
"ko": u"언어",
"lo": u"ພາສາ",
"lo-rLA": u"ພາສາ",
"lt": u"Kalba",
"lv": u"Valoda",
"mn": u"Хэл",
"mn-rMN": u"Хэл",
"ms": u"Bahasa",
"ms-rMY": u"Bahasa",
"nb": u"Språk",
"ne": u"भाषा",
"nl": u"Taal",
"pl": u"Język",
"pt": u"Idioma",
"pt-rPT": u"Idioma",
"ro": u"Limba",
"ru": u"Язык",
"si": u"භාෂාව",
"si-rLK": u"භාෂාව",
"sk": u"Jazyk",
"sl": u"Jezik",
"sr": u"Језик",
"sv": u"Språk",
"sw": u"Lugha",
"th": u"ภาษา",
"tl": u"Wika",
"tr": u"Dil",
"uk": u"Мова",
"vi": u"Ngôn ngữ",
"zh-rCN": u"语言",
"zh-rHK": u"語言",
"zh-rTW": u"語言",
"zu": u"Ulimi",
}
LANGUAGES = {
"en": u"English (United States)",
"es-rUS": u"Español (Estados Unidos)",
"af": u"Afrikaans", # Afrikaans
"af-rNA": u"Afrikaans (Namibië)", # Afrikaans (Namibia)
"af-rZA": u"Afrikaans (Suid-Afrika)", # Afrikaans (South Africa)
"agq": u"Aghem", # Aghem
"agq-rCM": u"Aghem (Kàmàlûŋ)", # Aghem (Cameroon)
"ak": u"Akan", # Akan
"ak-rGH": u"Akan (Gaana)", # Akan (Ghana)
"am": u"አማርኛ", # Amharic
"am-rET": u"አማርኛ (ኢትዮጵያ)", # Amharic (Ethiopia)
"ar": u"العربية", # Arabic
"ar_001": u"العربية (العالم)", # Arabic (World)
"ar-rAE": u"العربية (الإمارات العربية المتحدة)", # Arabic (United Arab Emirates)
"ar-rBH": u"العربية (البحرين)", # Arabic (Bahrain)
"ar-rDJ": u"العربية (جيبوتي)", # Arabic (Djibouti)
"ar-rDZ": u"العربية (الجزائر)", # Arabic (Algeria)
"ar-rEG": u"العربية (مصر)", # Arabic (Egypt)
"ar-rEH": u"العربية (الصحراء الغربية)", # Arabic (Western Sahara)
"ar-rER": u"العربية (أريتريا)", # Arabic (Eritrea)
"ar-rIL": u"العربية (إسرائيل)", # Arabic (Israel)
"ar-rIQ": u"العربية (العراق)", # Arabic (Iraq)
"ar-rJO": u"العربية (الأردن)", # Arabic (Jordan)
"ar-rKM": u"العربية (جزر القمر)", # Arabic (Comoros)
"ar-rKW": u"العربية (الكويت)", # Arabic (Kuwait)
"ar-rLB": u"العربية (لبنان)", # Arabic (Lebanon)
"ar-rLY": u"العربية (ليبيا)", # Arabic (Libya)
"ar-rMA": u"العربية (المغرب)", # Arabic (Morocco)
"ar-rMR": u"العربية (موريتانيا)", # Arabic (Mauritania)
"ar-rOM": u"العربية (عُمان)", # Arabic (Oman)
"ar-rPS": u"العربية (فلسطين)", # Arabic (Palestine)
"ar-rQA": u"العربية (قطر)", # Arabic (Qatar)
"ar-rSA": u"العربية (المملكة العربية السعودية)", # Arabic (Saudi Arabia)
"ar-rSD": u"العربية (السودان)", # Arabic (Sudan)
"ar-rSO": u"العربية (الصومال)", # Arabic (Somalia)
"ar-rSY": u"العربية (سوريا)", # Arabic (Syria)
"ar-rTD": u"العربية (تشاد)", # Arabic (Chad)
"ar-rTN": u"العربية (تونس)", # Arabic (Tunisia)
"ar-rYE": u"العربية (اليمن)", # Arabic (Yemen)
"as": u"অসমীয়া", # Assamese
"as-rIN": u"অসমীয়া (ভাৰত)", # Assamese (India)
"asa": u"Kipare", # Asu
"asa-rTZ": u"Kipare (Tadhania)", # Asu (Tanzania)
"az": u"Azərbaycanca", # Azerbaijani
"az-rCYRL": u"Азәрбајҹан (CYRL)", # Azerbaijani (CYRL)
"az-rCYRL_AZ": u"Азәрбајҹан (Азәрбајҹан,AZ)", # Azerbaijani (Azerbaijan,AZ)
"az-rLATN": u"Azərbaycanca (LATN)", # Azerbaijani (LATN)
"az-rLATN_AZ": u"Azərbaycanca (Azərbaycan,AZ)", # Azerbaijani (Azerbaijan,AZ)
"bas": u"Ɓàsàa", # Basaa
"bas-rCM": u"Ɓàsàa (Kàmɛ̀rûn)", # Basaa (Cameroon)
"be": u"беларуская", # Belarusian
"be-rBY": u"беларуская (Беларусь)", # Belarusian (Belarus)
"bem": u"Ichibemba", # Bemba
"bem-rZM": u"Ichibemba (Zambia)", # Bemba (Zambia)
"bez": u"Hibena", # Bena
"bez-rTZ": u"Hibena (Hutanzania)", # Bena (Tanzania)
"bg": u"български", # Bulgarian
"bg-rBG": u"български (България)", # Bulgarian (Bulgaria)
"bm": u"Bamanakan", # Bambara
"bm-rML": u"Bamanakan (Mali)", # Bambara (Mali)
"bn": u"বাংলা", # Bengali
"bn-rBD": u"বাংলা (বাংলাদেশ)", # Bengali (Bangladesh)
"bn-rIN": u"বাংলা (ভারত)", # Bengali (India)
"bo": u"པོད་སྐད་", # Tibetan
"bo-rCN": u"པོད་སྐད་ (རྒྱ་ནག)", # Tibetan (China)
"bo-rIN": u"པོད་སྐད་ (རྒྱ་གར་)", # Tibetan (India)
"br": u"Brezhoneg", # Breton
"br-rFR": u"Brezhoneg (Frañs)", # Breton (France)
"brx": u"बड़ो", # Bodo
"brx-rIN": u"बड़ो (भारत)", # Bodo (India)
"bs": u"Bosanski", # Bosnian
"bs-rCYRL": u"босански (CYRL)", # Bosnian (CYRL)
"bs-rCYRL_BA": u"босански (Босна и Херцеговина,BA)", # Bosnian (Bosnia and Herzegovina,BA)
"bs-rLATN": u"Bosanski (LATN)", # Bosnian (LATN)
"bs-rLATN_BA": u"Bosanski (Bosna i Hercegovina,BA)", # Bosnian (Bosnia and Herzegovina,BA)
"ca": u"Català", # Catalan
"ca-rAD": u"Català (Andorra)", # Catalan (Andorra)
"ca-rES": u"Català (Espanya)", # Catalan (Spain)
"cgg": u"Rukiga", # Chiga
"cgg-rUG": u"Rukiga (Uganda)", # Chiga (Uganda)
"chr": u"ᏣᎳᎩ", # Cherokee
"chr-rUS": u"ᏣᎳᎩ (ᎠᎹᏰᏟ)", # Cherokee (United States)
"cs": u"čeština", # Czech
"cs-rCZ": u"čeština (Česká republika)", # Czech (Czech Republic)
"cy": u"Cymraeg", # Welsh
"cy-rGB": u"Cymraeg (y Deyrnas Unedig)", # Welsh (United Kingdom)
"da": u"Dansk", # Danish
"da-rDK": u"Dansk (Danmark)", # Danish (Denmark)
"dav": u"Kitaita", # Taita
"dav-rKE": u"Kitaita (Kenya)", # Taita (Kenya)
"de": u"Deutsch", # German
"de-rAT": u"Deutsch (Österreich)", # German (Austria)
"de-rBE": u"Deutsch (Belgien)", # German (Belgium)
"de-rCH": u"Deutsch (Schweiz)", # German (Switzerland)
"de-rDE": u"Deutsch (Deutschland)", # German (Germany)
"de-rLI": u"Deutsch (Liechtenstein)", # German (Liechtenstein)
"de-rLU": u"Deutsch (Luxemburg)", # German (Luxembourg)
"dje": u"Zarmaciine", # Zarma
"dje-rNE": u"Zarmaciine (Nižer)", # Zarma (Niger)
"dua": u"Duálá", # Duala
"dua-rCM": u"Duálá (Cameroun)", # Duala (Cameroon)
"dyo": u"Joola", # Jola-Fonyi
"dyo-rSN": u"Joola (Senegal)", # Jola-Fonyi (Senegal)
"dz": u"རྫོང་ཁ", # Dzongkha
"dz-rBT": u"རྫོང་ཁ (འབྲུག)", # Dzongkha (Bhutan)
"ebu": u"Kĩembu", # Embu
"ebu-rKE": u"Kĩembu (Kenya)", # Embu (Kenya)
"ee": u"Eʋegbe", # Ewe
"ee-rGH": u"Eʋegbe (Ghana nutome)", # Ewe (Ghana)
"ee-rTG": u"Eʋegbe (Togo nutome)", # Ewe (Togo)
"el": u"Ελληνικά", # Greek
"el-rCY": u"Ελληνικά (Κύπρος)", # Greek (Cyprus)
"el-rGR": u"Ελληνικά (Ελλάδα)", # Greek (Greece)
"en": u"English", # English
"en_150": u"English (Europe)", # English (Europe)
"en-rAG": u"English (Antigua and Barbuda)", # English (Antigua and Barbuda)
"en-rAS": u"English (American Samoa)", # English (American Samoa)
"en-rAU": u"English (Australia)", # English (Australia)
"en-rBB": u"English (Barbados)", # English (Barbados)
"en-rBE": u"English (Belgium)", # English (Belgium)
"en-rBM": u"English (Bermuda)", # English (Bermuda)
"en-rBS": u"English (Bahamas)", # English (Bahamas)
"en-rBW": u"English (Botswana)", # English (Botswana)
"en-rBZ": u"English (Belize)", # English (Belize)
"en-rCA": u"English (Canada)", # English (Canada)
"en-rCM": u"English (Cameroon)", # English (Cameroon)
"en-rDM": u"English (Dominica)", # English (Dominica)
"en-rFJ": u"English (Fiji)", # English (Fiji)
"en-rFM": u"English (Micronesia)", # English (Micronesia)
"en-rGB": u"English (United Kingdom)", # English (United Kingdom)
"en-rGD": u"English (Grenada)", # English (Grenada)
"en-rGG": u"English (Guernsey)", # English (Guernsey)
"en-rGH": u"English (Ghana)", # English (Ghana)
"en-rGI": u"English (Gibraltar)", # English (Gibraltar)
"en-rGM": u"English (Gambia)", # English (Gambia)
"en-rGU": u"English (Guam)", # English (Guam)
"en-rGY": u"English (Guyana)", # English (Guyana)
"en-rHK": u"English (Hong Kong)", # English (Hong Kong)
"en-rIE": u"English (Ireland)", # English (Ireland)
"en-rIM": u"English (Isle of Man)", # English (Isle of Man)
"en-rIN": u"English (India)", # English (India)
"en-rJE": u"English (Jersey)", # English (Jersey)
"en-rJM": u"English (Jamaica)", # English (Jamaica)
"en-rKE": u"English (Kenya)", # English (Kenya)
"en-rKI": u"English (Kiribati)", # English (Kiribati)
"en-rKN": u"English (Saint Kitts and Nevis)", # English (Saint Kitts and Nevis)
"en-rKY": u"English (Cayman Islands)", # English (Cayman Islands)
"en-rLC": u"English (Saint Lucia)", # English (Saint Lucia)
"en-rLR": u"English (Liberia)", # English (Liberia)
"en-rLS": u"English (Lesotho)", # English (Lesotho)
"en-rMG": u"English (Madagascar)", # English (Madagascar)
"en-rMH": u"English (Marshall Islands)", # English (Marshall Islands)
"en-rMP": u"English (Northern Mariana Islands)", # English (Northern Mariana Islands)
"en-rMT": u"English (Malta)", # English (Malta)
"en-rMU": u"English (Mauritius)", # English (Mauritius)
"en-rMW": u"English (Malawi)", # English (Malawi)
"en-rNA": u"English (Namibia)", # English (Namibia)
"en-rNG": u"English (Nigeria)", # English (Nigeria)
"en-rNZ": u"English (New Zealand)", # English (New Zealand)
"en-rPG": u"English (Papua New Guinea)", # English (Papua New Guinea)
"en-rPH": u"English (Philippines)", # English (Philippines)
"en-rPK": u"English (Pakistan)", # English (Pakistan)
"en-rPR": u"English (Puerto Rico)", # English (Puerto Rico)
"en-rPW": u"English (Palau)", # English (Palau)
"en-rSB": u"English (Solomon Islands)", # English (Solomon Islands)
"en-rSC": u"English (Seychelles)", # English (Seychelles)
"en-rSG": u"English (Singapore)", # English (Singapore)
"en-rSL": u"English (Sierra Leone)", # English (Sierra Leone)
"en-rSS": u"English (South Sudan)", # English (South Sudan)
"en-rSZ": u"English (Swaziland)", # English (Swaziland)
"en-rTC": u"English (Turks and Caicos Islands)", # English (Turks and Caicos Islands)
"en-rTO": u"English (Tonga)", # English (Tonga)
"en-rTT": u"English (Trinidad and Tobago)", # English (Trinidad and Tobago)
"en-rTZ": u"English (Tanzania)", # English (Tanzania)
"en-rUG": u"English (Uganda)", # English (Uganda)
"en-rUM": u"English (U.S. Outlying Islands)", # English (U.S. Outlying Islands)
"en-rUS": u"English (United States)", # English (United States)
"en-rUS_POSIX": u"English (United States,Computer)", # English (United States,Computer)
"en-rVC": u"English (Saint Vincent and the Grenadines)", # English (Saint Vincent and the Grenadines)
"en-rVG": u"English (British Virgin Islands)", # English (British Virgin Islands)
"en-rVI": u"English (U.S. Virgin Islands)", # English (U.S. Virgin Islands)
"en-rVU": u"English (Vanuatu)", # English (Vanuatu)
"en-rWS": u"English (Samoa)", # English (Samoa)
"en-rZA": u"English (South Africa)", # English (South Africa)
"en-rZM": u"English (Zambia)", # English (Zambia)
"en-rZW": u"English (Zimbabwe)", # English (Zimbabwe)
"eo": u"Esperanto", # Esperanto
"es": u"Español", # Spanish
"es_419": u"Español (Latinoamérica)", # Spanish (Latin America)
"es-rAR": u"Español (Argentina)", # Spanish (Argentina)
"es-rBO": u"Español (Bolivia)", # Spanish (Bolivia)
"es-rCL": u"Español (Chile)", # Spanish (Chile)
"es-rCO": u"Español (Colombia)", # Spanish (Colombia)
"es-rCR": u"Español (Costa Rica)", # Spanish (Costa Rica)
"es-rCU": u"Español (Cuba)", # Spanish (Cuba)
"es-rDO": u"Español (República Dominicana)", # Spanish (Dominican Republic)
"es-rEA": u"Español (Ceuta y Melilla)", # Spanish (Ceuta and Melilla)
"es-rEC": u"Español (Ecuador)", # Spanish (Ecuador)
"es-rES": u"Español (España)", # Spanish (Spain)
"es-rGQ": u"Español (Guinea Ecuatorial)", # Spanish (Equatorial Guinea)
"es-rGT": u"Español (Guatemala)", # Spanish (Guatemala)
"es-rHN": u"Español (Honduras)", # Spanish (Honduras)
"es-rIC": u"Español (Islas Canarias)", # Spanish (Canary Islands)
"es-rMX": u"Español (México)", # Spanish (Mexico)
"es-rNI": u"Español (Nicaragua)", # Spanish (Nicaragua)
"es-rPA": u"Español (Panamá)", # Spanish (Panama)
"es-rPE": u"Español (Perú)", # Spanish (Peru)
"es-rPH": u"Español (Filipinas)", # Spanish (Philippines)
"es-rPR": u"Español (Puerto Rico)", # Spanish (Puerto Rico)
"es-rPY": u"Español (Paraguay)", # Spanish (Paraguay)
"es-rSV": u"Español (El Salvador)", # Spanish (El Salvador)
"es-rUS": u"Español (Estados Unidos)", # Spanish (United States)
"es-rUY": u"Español (Uruguay)", # Spanish (Uruguay)
"es-rVE": u"Español (Venezuela)", # Spanish (Venezuela)
"et": u"Eesti", # Estonian
"et-rEE": u"Eesti (Eesti)", # Estonian (Estonia)
"eu": u"Euskara", # Basque
"eu-rES": u"Euskara (Espainia)", # Basque (Spain)
"ewo": u"Ewondo", # Ewondo
"ewo-rCM": u"Ewondo (Kamərún)", # Ewondo (Cameroon)
"fa": u"فارسی", # Persian
"fa-rAF": u"دری (افغانستان)", # Persian (Afghanistan)
"fa-rIR": u"فارسی (ایران)", # Persian (Iran)
"ff": u"Pulaar", # Fulah
"ff-rSN": u"Pulaar (Senegaal)", # Fulah (Senegal)
"fi": u"Suomi", # Finnish
"fi-rFI": u"Suomi (Suomi)", # Finnish (Finland)
"fil": u"Filipino", # Filipino
"fil-rPH": u"Filipino (Pilipinas)", # Filipino (Philippines)
"fo": u"Føroyskt", # Faroese
"fo-rFO": u"Føroyskt (Føroyar)", # Faroese (Faroe Islands)
"fr": u"Français", # French
"fr-rBE": u"Français (Belgique)", # French (Belgium)
"fr-rBF": u"Français (Burkina Faso)", # French (Burkina Faso)
"fr-rBI": u"Français (Burundi)", # French (Burundi)
"fr-rBJ": u"Français (Bénin)", # French (Benin)
"fr-rBL": u"Français (Saint-Barthélémy)", # French (Saint Barthélemy)
"fr-rCA": u"Français (Canada)", # French (Canada)
"fr-rCD": u"Français (République démocratique du Congo)", # French (Congo [DRC])
"fr-rCF": u"Français (République centrafricaine)", # French (Central African Republic)
"fr-rCG": u"Français (Congo-Brazzaville)", # French (Congo [Republic])
"fr-rCH": u"Français (Suisse)", # French (Switzerland)
"fr-rCI": u"Français (Côte d’Ivoire)", # French (Côte d’Ivoire)
"fr-rCM": u"Français (Cameroun)", # French (Cameroon)
"fr-rDJ": u"Français (Djibouti)", # French (Djibouti)
"fr-rDZ": u"Français (Algérie)", # French (Algeria)
"fr-rFR": u"Français (France)", # French (France)
"fr-rGA": u"Français (Gabon)", # French (Gabon)
"fr-rGF": u"Français (Guyane française)", # French (French Guiana)
"fr-rGN": u"Français (Guinée)", # French (Guinea)
"fr-rGP": u"Français (Guadeloupe)", # French (Guadeloupe)
"fr-rGQ": u"Français (Guinée équatoriale)", # French (Equatorial Guinea)
"fr-rHT": u"Français (Haïti)", # French (Haiti)
"fr-rKM": u"Français (Comores)", # French (Comoros)
"fr-rLU": u"Français (Luxembourg)", # French (Luxembourg)
"fr-rMA": u"Français (Maroc)", # French (Morocco)
"fr-rMC": u"Français (Monaco)", # French (Monaco)
"fr-rMF": u"Français (Saint-Martin [partie française])", # French (Saint Martin)
"fr-rMG": u"Français (Madagascar)", # French (Madagascar)
"fr-rML": u"Français (Mali)", # French (Mali)
"fr-rMQ": u"Français (Martinique)", # French (Martinique)
"fr-rMR": u"Français (Mauritanie)", # French (Mauritania)
"fr-rMU": u"Français (Maurice)", # French (Mauritius)
"fr-rNC": u"Français (Nouvelle-Calédonie)", # French (New Caledonia)
"fr-rNE": u"Français (Niger)", # French (Niger)
"fr-rPF": u"Français (Polynésie française)", # French (French Polynesia)
"fr-rRE": u"Français (Réunion)", # French (Réunion)
"fr-rRW": u"Français (Rwanda)", # French (Rwanda)
"fr-rSC": u"Français (Seychelles)", # French (Seychelles)
"fr-rSN": u"Français (Sénégal)", # French (Senegal)
"fr-rSY": u"Français (Syrie)", # French (Syria)
"fr-rTD": u"Français (Tchad)", # French (Chad)
"fr-rTG": u"Français (Togo)", # French (Togo)
"fr-rTN": u"Français (Tunisie)", # French (Tunisia)
"fr-rVU": u"Français (Vanuatu)", # French (Vanuatu)
"fr-rYT": u"Français (Mayotte)", # French (Mayotte)
"ga": u"Gaeilge", # Irish
"ga-rIE": u"Gaeilge (Éire)", # Irish (Ireland)
"gl": u"Galego", # Galician
"gl-rES": u"Galego (España)", # Galician (Spain)
"gsw": u"Schwiizertüütsch", # Swiss German
"gsw-rCH": u"Schwiizertüütsch (Schwiiz)", # Swiss German (Switzerland)
"gu": u"ગુજરાતી", # Gujarati
"gu-rIN": u"ગુજરાતી (ભારત)", # Gujarati (India)
"guz": u"Ekegusii", # Gusii
"guz-rKE": u"Ekegusii (Kenya)", # Gusii (Kenya)
"gv": u"Gaelg", # Manx
"gv-rGB": u"Gaelg (Rywvaneth Unys)", # Manx (United Kingdom)
"ha": u"Hausa", # Hausa
"ha-rLATN": u"Hausa (LATN)", # Hausa (LATN)
"ha-rLATN_GH": u"Hausa (Gana,GH)", # Hausa (Ghana,GH)
"ha-rLATN_NE": u"Hausa (Nijar,NE)", # Hausa (Niger,NE)
"ha-rLATN_NG": u"Hausa (Najeriya,NG)", # Hausa (Nigeria,NG)
"haw": u"ʻŌlelo Hawaiʻi", # Hawaiian
"haw-rUS": u"ʻŌlelo Hawaiʻi (ʻAmelika Hui Pū ʻIa)", # Hawaiian (United States)
"iw": u"עברית", # Hebrew
"iw-rIL": u"עברית (ישראל)", # Hebrew (Israel)
"hi": u"हिन्दी", # Hindi
"hi-rIN": u"हिन्दी (भारत)", # Hindi (India)
"hr": u"Hrvatski", # Croatian
"hr-rBA": u"Hrvatski (Bosna i Hercegovina)", # Croatian (Bosnia and Herzegovina)
"hr-rHR": u"Hrvatski (Hrvatska)", # Croatian (Croatia)
"hu": u"Magyar", # Hungarian
"hu-rHU": u"Magyar (Magyarország)", # Hungarian (Hungary)
"hy": u"հայերեն", # Armenian
"hy-rAM": u"հայերեն (Հայաստան)", # Armenian (Armenia)
"in": u"Bahasa Indonesia", # Indonesian
"in-rID": u"Bahasa Indonesia (Indonesia)", # Indonesian (Indonesia)
"ig": u"Igbo", # Igbo
"ig-rNG": u"Igbo (Nigeria)", # Igbo (Nigeria)
"ii": u"ꆈꌠꉙ", # Sichuan Yi
"ii-rCN": u"ꆈꌠꉙ (ꍏꇩ)", # Sichuan Yi (China)
"is": u"íslenska", # Icelandic
"is-rIS": u"íslenska (Ísland)", # Icelandic (Iceland)
"it": u"Italiano", # Italian
"it-rCH": u"Italiano (Svizzera)", # Italian (Switzerland)
"it-rIT": u"Italiano (Italia)", # Italian (Italy)
"it-rSM": u"Italiano (San Marino)", # Italian (San Marino)
"ja": u"日本語", # Japanese
"ja-rJP": u"日本語 (日本)", # Japanese (Japan)
"jgo": u"Ndaꞌa", # Ngomba
"jgo-rCM": u"Ndaꞌa (Kamɛlûn)", # Ngomba (Cameroon)
"jmc": u"Kimachame", # Machame
"jmc-rTZ": u"Kimachame (Tanzania)", # Machame (Tanzania)
"ka": u"ქართული", # Georgian
"ka-rGE": u"ქართული (საქართველო)", # Georgian (Georgia)
"kab": u"Taqbaylit", # Kabyle
"kab-rDZ": u"Taqbaylit (Lezzayer)", # Kabyle (Algeria)
"kam": u"Kikamba", # Kamba
"kam-rKE": u"Kikamba (Kenya)", # Kamba (Kenya)
"kde": u"Chimakonde", # Makonde
"kde-rTZ": u"Chimakonde (Tanzania)", # Makonde (Tanzania)
"kea": u"Kabuverdianu", # Kabuverdianu
"kea-rCV": u"Kabuverdianu (Kabu Verdi)", # Kabuverdianu (Cape Verde)
"khq": u"Koyra ciini", # Koyra Chiini
"khq-rML": u"Koyra ciini (Maali)", # Koyra Chiini (Mali)
"ki": u"Gikuyu", # Kikuyu
"ki-rKE": u"Gikuyu (Kenya)", # Kikuyu (Kenya)
"kk": u"қазақ тілі", # Kazakh
"kk-rCYRL": u"қазақ тілі (CYRL)", # Kazakh (CYRL)
"kk-rCYRL_KZ": u"қазақ тілі (Қазақстан,KZ)", # Kazakh (Kazakhstan,KZ)
"kl": u"Kalaallisut", # Kalaallisut
"kl-rGL": u"Kalaallisut (Kalaallit Nunaat)", # Kalaallisut (Greenland)
"kln": u"Kalenjin", # Kalenjin
"kln-rKE": u"Kalenjin (Emetab Kenya)", # Kalenjin (Kenya)
"km": u"ខ្មែរ", # Khmer
"km-rKH": u"ខ្មែរ (កម្ពុជា)", # Khmer (Cambodia)
"kn": u"ಕನ್ನಡ", # Kannada
"kn-rIN": u"ಕನ್ನಡ (ಭಾರತ)", # Kannada (India)
"ko": u"한국어", # Korean
"ko-rKP": u"한국어 (조선 민주주의 인민 공화국)", # Korean (North Korea)
"ko-rKR": u"한국어 (대한민국)", # Korean (South Korea)
"kok": u"कोंकणी", # Konkani
"kok-rIN": u"कोंकणी (भारत)", # Konkani (India)
"ks": u"کٲشُر", # Kashmiri
"ks-rARAB": u"کٲشُر (ARAB)", # Kashmiri (ARAB)
"ks-rARAB_IN": u"کٲشُر (ہِنٛدوستان,IN)", # Kashmiri (India,IN)
"ksb": u"Kishambaa", # Shambala
"ksb-rTZ": u"Kishambaa (Tanzania)", # Shambala (Tanzania)
"ksf": u"Rikpa", # Bafia
"ksf-rCM": u"Rikpa (kamɛrún)", # Bafia (Cameroon)
"kw": u"Kernewek", # Cornish
"kw-rGB": u"Kernewek (Rywvaneth Unys)", # Cornish (United Kingdom)
"lag": u"Kɨlaangi", # Langi
"lag-rTZ": u"Kɨlaangi (Taansanía)", # Langi (Tanzania)
"lg": u"Luganda", # Ganda
"lg-rUG": u"Luganda (Yuganda)", # Ganda (Uganda)
"ln": u"Lingála", # Lingala
"ln-rAO": u"Lingála (Angóla)", # Lingala (Angola)
"ln-rCD": u"Lingála (Repibiki demokratiki ya Kongó)", # Lingala (Congo [DRC])
"ln-rCF": u"Lingála (Repibiki ya Afríka ya Káti)", # Lingala (Central African Republic)
"ln-rCG": u"Lingála (Kongo)", # Lingala (Congo [Republic])
"lo": u"ລາວ", # Lao
"lo-rLA": u"ລາວ (ສ.ປ.ປ ລາວ)", # Lao (Laos)
"lt": u"Lietuvių", # Lithuanian
"lt-rLT": u"Lietuvių (Lietuva)", # Lithuanian (Lithuania)
"lu": u"Tshiluba", # Luba-Katanga
"lu-rCD": u"Tshiluba (Ditunga wa Kongu)", # Luba-Katanga (Congo [DRC])
"luo": u"Dholuo", # Luo
"luo-rKE": u"Dholuo (Kenya)", # Luo (Kenya)
"luy": u"Luluhia", # Luyia
"luy-rKE": u"Luluhia (Kenya)", # Luyia (Kenya)
"lv": u"Latviešu", # Latvian
"lv-rLV": u"Latviešu (Latvija)", # Latvian (Latvia)
"mas": u"Maa", # Masai
"mas-rKE": u"Maa (Kenya)", # Masai (Kenya)
"mas-rTZ": u"Maa (Tansania)", # Masai (Tanzania)
"mer": u"Kĩmĩrũ", # Meru
"mer-rKE": u"Kĩmĩrũ (Kenya)", # Meru (Kenya)
"mfe": u"Kreol morisien", # Morisyen
"mfe-rMU": u"Kreol morisien (Moris)", # Morisyen (Mauritius)
"mg": u"Malagasy", # Malagasy
"mg-rMG": u"Malagasy (Madagasikara)", # Malagasy (Madagascar)
"mgh": u"Makua", # Makhuwa-Meetto
"mgh-rMZ": u"Makua (Umozambiki)", # Makhuwa-Meetto (Mozambique)
"mgo": u"Metaʼ", # Meta'
"mgo-rCM": u"Metaʼ (Kamalun)", # Meta' (Cameroon)
"mk": u"македонски", # Macedonian
"mk-rMK": u"македонски (Македонија)", # Macedonian (Macedonia [FYROM])
"ml": u"മലയാളം", # Malayalam
"ml-rIN": u"മലയാളം (ഇന്ത്യ)", # Malayalam (India)
"mn": u"монгол", # Mongolian
"mn-rCYRL": u"монгол (CYRL)", # Mongolian (CYRL)
"mn-rCYRL_MN": u"монгол (Монгол,MN)", # Mongolian (Mongolia,MN)
"mr": u"मराठी", # Marathi
"mr-rIN": u"मराठी (भारत)", # Marathi (India)
"ms": u"Bahasa Melayu", # Malay
"ms-rLATN": u"Bahasa Melayu (LATN)", # Malay (LATN)
"ms-rLATN_BN": u"Bahasa Melayu (Brunei,BN)", # Malay (Brunei,BN)
"ms-rLATN_MY": u"Bahasa Melayu (Malaysia,MY)", # Malay (Malaysia,MY)
"ms-rLATN_SG": u"Bahasa Melayu (Singapura,SG)", # Malay (Singapore,SG)
"mt": u"Malti", # Maltese
"mt-rMT": u"Malti (Malta)", # Maltese (Malta)
"mua": u"MUNDAŊ", # Mundang
"mua-rCM": u"MUNDAŊ (kameruŋ)", # Mundang (Cameroon)
"my": u"ဗမာ", # Burmese
"my-rMM": u"ဗမာ (မြန်မာ)", # Burmese (Myanmar [Burma])
"naq": u"Khoekhoegowab", # Nama
"naq-rNA": u"Khoekhoegowab (Namibiab)", # Nama (Namibia)
"nb": u"Norsk bokmål", # Norwegian Bokmål
"nb-rNO": u"Norsk bokmål (Norge)", # Norwegian Bokmål (Norway)
"nd": u"IsiNdebele", # North Ndebele
"nd-rZW": u"IsiNdebele (Zimbabwe)", # North Ndebele (Zimbabwe)
"ne": u"नेपाली", # Nepali
"ne-rIN": u"नेपाली (भारत)", # Nepali (India)
"ne-rNP": u"नेपाली (नेपाल)", # Nepali (Nepal)
"nl": u"Nederlands", # Dutch
"nl-rAW": u"Nederlands (Aruba)", # Dutch (Aruba)
"nl-rBE": u"Nederlands (België)", # Dutch (Belgium)
"nl-rCW": u"Nederlands (Curaçao)", # Dutch (Curaçao)
"nl-rNL": u"Nederlands (Nederland)", # Dutch (Netherlands)
"nl-rSR": u"Nederlands (Suriname)", # Dutch (Suriname)
"nl-rSX": u"Nederlands (Sint-Maarten)", # Dutch (Sint Maarten)
"nmg": u"Nmg", # Kwasio
"nmg-rCM": u"Nmg (Kamerun)", # Kwasio (Cameroon)
"nn": u"Nynorsk", # Norwegian Nynorsk
"nn-rNO": u"Nynorsk (Noreg)", # Norwegian Nynorsk (Norway)
"nus": u"Thok Nath", # Nuer
"nus-rSD": u"Thok Nath (Sudan)", # Nuer (Sudan)
"nyn": u"Runyankore", # Nyankole
"nyn-rUG": u"Runyankore (Uganda)", # Nyankole (Uganda)
"om": u"Oromoo", # Oromo
"om-rET": u"Oromoo (Itoophiyaa)", # Oromo (Ethiopia)
"om-rKE": u"Oromoo (Keeniyaa)", # Oromo (Kenya)
"or": u"ଓଡ଼ିଆ", # Oriya
"or-rIN": u"ଓଡ଼ିଆ (ଭାରତ)", # Oriya (India)
"pa": u"ਪੰਜਾਬੀ", # Punjabi
"pa-rARAB": u"پنجاب (ARAB)", # Punjabi (ARAB)
"pa-rARAB_PK": u"پنجاب (پکستان,PK)", # Punjabi (Pakistan,PK)
"pa-rGURU": u"ਪੰਜਾਬੀ (GURU)", # Punjabi (GURU)
"pa-rGURU_IN": u"ਪੰਜਾਬੀ (ਭਾਰਤ,IN)", # Punjabi (India,IN)
"pl": u"Polski", # Polish
"pl-rPL": u"Polski (Polska)", # Polish (Poland)
"ps": u"پښتو", # Pashto
"ps-rAF": u"پښتو (افغانستان)", # Pashto (Afghanistan)
"pt": u"Português", # Portuguese
"pt-rAO": u"Português (Angola)", # Portuguese (Angola)
"pt-rBR": u"Português (Brasil)", # Portuguese (Brazil)
"pt-rCV": u"Português (Cabo Verde)", # Portuguese (Cape Verde)
"pt-rGW": u"Português (Guiné Bissau)", # Portuguese (Guinea-Bissau)
"pt-rMO": u"Português (Macau)", # Portuguese (Macau)
"pt-rMZ": u"Português (Moçambique)", # Portuguese (Mozambique)
"pt-rPT": u"Português (Portugal)", # Portuguese (Portugal)
"pt-rST": u"Português (São Tomé e Príncipe)", # Portuguese (São Tomé and Príncipe)
"pt-rTL": u"Português (Timor-Leste)", # Portuguese (Timor-Leste)
"rm": u"Rumantsch", # Romansh
"rm-rCH": u"Rumantsch (Svizra)", # Romansh (Switzerland)
"rn": u"Ikirundi", # Rundi
"rn-rBI": u"Ikirundi (Uburundi)", # Rundi (Burundi)
"ro": u"Română", # Romanian
"ro-rMD": u"Română (Republica Moldova)", # Romanian (Moldova)
"ro-rRO": u"Română (România)", # Romanian (Romania)
"rof": u"Kihorombo", # Rombo
"rof-rTZ": u"Kihorombo (Tanzania)", # Rombo (Tanzania)
"ru": u"русский", # Russian
"ru-rBY": u"русский (Беларусь)", # Russian (Belarus)
"ru-rKG": u"русский (Киргизия)", # Russian (Kyrgyzstan)
"ru-rKZ": u"русский (Казахстан)", # Russian (Kazakhstan)
"ru-rMD": u"русский (Молдова)", # Russian (Moldova)
"ru-rRU": u"русский (Россия)", # Russian (Russia)
"ru-rUA": u"русский (Украина)", # Russian (Ukraine)
"rw": u"Kinyarwanda", # Kinyarwanda
"rw-rRW": u"Kinyarwanda (Rwanda)", # Kinyarwanda (Rwanda)
"rwk": u"Kiruwa", # Rwa
"rwk-rTZ": u"Kiruwa (Tanzania)", # Rwa (Tanzania)
"saq": u"Kisampur", # Samburu
"saq-rKE": u"Kisampur (Kenya)", # Samburu (Kenya)
"sbp": u"Ishisangu", # Sangu
"sbp-rTZ": u"Ishisangu (Tansaniya)", # Sangu (Tanzania)
"seh": u"Sena", # Sena
"seh-rMZ": u"Sena (Moçambique)", # Sena (Mozambique)
"ses": u"Koyraboro senni", # Koyraboro Senni
"ses-rML": u"Koyraboro senni (Maali)", # Koyraboro Senni (Mali)
"sg": u"Sängö", # Sango
"sg-rCF": u"Sängö (Ködörösêse tî Bêafrîka)", # Sango (Central African Republic)
"shi": u"ⵜⴰⵎⴰⵣⵉⵖⵜ", # Tachelhit
"shi-rLATN": u"Tamazight (LATN)", # Tachelhit (LATN)
"shi-rLATN_MA": u"Tamazight (lmɣrib,MA)", # Tachelhit (Morocco,MA)
"shi-rTFNG": u"ⵜⴰⵎⴰⵣⵉⵖⵜ (TFNG)", # Tachelhit (TFNG)
"shi-rTFNG_MA": u"ⵜⴰⵎⴰⵣⵉⵖⵜ (ⵍⵎⵖⵔⵉⴱ,MA)", # Tachelhit (Morocco,MA)
"si": u"සිංහල", # Sinhala
"si-rLK": u"සිංහල (ශ්‍රී ලංකාව)", # Sinhala (Sri Lanka)
"sk": u"Slovenčina", # Slovak
"sk-rSK": u"Slovenčina (Slovensko)", # Slovak (Slovakia)
"sl": u"Slovenščina", # Slovenian
"sl-rSI": u"Slovenščina (Slovenija)", # Slovenian (Slovenia)
"sn": u"ChiShona", # Shona
"sn-rZW": u"ChiShona (Zimbabwe)", # Shona (Zimbabwe)
"so": u"Soomaali", # Somali
"so-rDJ": u"Soomaali (Jabuuti)", # Somali (Djibouti)
"so-rET": u"Soomaali (Itoobiya)", # Somali (Ethiopia)
"so-rKE": u"Soomaali (Kiiniya)", # Somali (Kenya)
"so-rSO": u"Soomaali (Soomaaliya)", # Somali (Somalia)
"sq": u"Shqip", # Albanian
"sq-rAL": u"Shqip (Shqipëria)", # Albanian (Albania)
"sq-rMK": u"Shqip (Maqedoni)", # Albanian (Macedonia [FYROM])
"sr": u"Српски", # Serbian
"sr-rCYRL": u"Српски (CYRL)", # Serbian (CYRL)
"sr-rCYRL_BA": u"Српски (Босна и Херцеговина,BA)", # Serbian (Bosnia and Herzegovina,BA)
"sr-rCYRL_ME": u"Српски (Црна Гора,ME)", # Serbian (Montenegro,ME)
"sr-rCYRL_RS": u"Српски (Србија,RS)", # Serbian (Serbia,RS)
"sr-rLATN": u"Srpski (LATN)", # Serbian (LATN)
"sr-rLATN_BA": u"Srpski (Bosna i Hercegovina,BA)", # Serbian (Bosnia and Herzegovina,BA)
"sr-rLATN_ME": u"Srpski (Crna Gora,ME)", # Serbian (Montenegro,ME)
"sr-rLATN_RS": u"Srpski (Srbija,RS)", # Serbian (Serbia,RS)
"sv": u"Svenska", # Swedish
"sv-rAX": u"Svenska (Åland)", # Swedish (Åland Islands)
"sv-rFI": u"Svenska (Finland)", # Swedish (Finland)
"sv-rSE": u"Svenska (Sverige)", # Swedish (Sweden)
"sw": u"Kiswahili", # Swahili
"sw-rKE": u"Kiswahili (Kenya)", # Swahili (Kenya)
"sw-rTZ": u"Kiswahili (Tanzania)", # Swahili (Tanzania)
"sw-rUG": u"Kiswahili (Uganda)", # Swahili (Uganda)
"swc": u"Kiswahili ya Kongo", # Congo Swahili
"swc-rCD": u"Kiswahili ya Kongo (Jamhuri ya Kidemokrasia ya Kongo)", # Congo Swahili (Congo [DRC])
"ta": u"தமிழ்", # Tamil
"ta-rIN": u"தமிழ் (இந்தியா)", # Tamil (India)
"ta-rLK": u"தமிழ் (இலங்கை)", # Tamil (Sri Lanka)
"ta-rMY": u"தமிழ் (மலேஷியா)", # Tamil (Malaysia)
"ta-rSG": u"தமிழ் (சிங்கப்பூர்)", # Tamil (Singapore)
"te": u"తెలుగు", # Telugu
"te-rIN": u"తెలుగు (భారత దేశం)", # Telugu (India)
"teo": u"Kiteso", # Teso
"teo-rKE": u"Kiteso (Kenia)", # Teso (Kenya)
"teo-rUG": u"Kiteso (Uganda)", # Teso (Uganda)
"th": u"ไทย", # Thai
"th-rTH": u"ไทย (ไทย)", # Thai (Thailand)
"ti": u"ትግርኛ", # Tigrinya
"ti-rER": u"ትግርኛ (ER)", # Tigrinya (Eritrea)
"ti-rET": u"ትግርኛ (ET)", # Tigrinya (Ethiopia)
"to": u"Lea fakatonga", # Tongan
"to-rTO": u"Lea fakatonga (Tonga)", # Tongan (Tonga)
"tr": u"Türkçe", # Turkish
"tr-rCY": u"Türkçe (Güney Kıbrıs Rum Kesimi)", # Turkish (Cyprus)
"tr-rTR": u"Türkçe (Türkiye)", # Turkish (Turkey)
"twq": u"Tasawaq senni", # Tasawaq
"twq-rNE": u"Tasawaq senni (Nižer)", # Tasawaq (Niger)
"tzm": u"Tamaziɣt", # Central Atlas Tamazight
"tzm-rLATN": u"Tamaziɣt (LATN)", # Central Atlas Tamazight (LATN)
"tzm-rLATN_MA": u"Tamaziɣt (Meṛṛuk,MA)", # Central Atlas Tamazight (Morocco,MA)
"uk": u"українська", # Ukrainian
"uk-rUA": u"українська (Україна)", # Ukrainian (Ukraine)
"ur": u"اردو", # Urdu
"ur-rIN": u"اردو (بھارت)", # Urdu (India)
"ur-rPK": u"اردو (پاکستان)", # Urdu (Pakistan)
"uz": u"Ўзбек", # Uzbek
"uz-rARAB": u"اوزبیک (ARAB)", # Uzbek (ARAB)
"uz-rARAB_AF": u"اوزبیک (افغانستان,AF)", # Uzbek (Afghanistan,AF)
"uz-rCYRL": u"Ўзбек (CYRL)", # Uzbek (CYRL)
"uz-rCYRL_UZ": u"Ўзбек (Ўзбекистон,UZ)", # Uzbek (Uzbekistan,UZ)
"uz-rLATN": u"Oʻzbekcha (LATN)", # Uzbek (LATN)
"uz-rLATN_UZ": u"Oʻzbekcha (Oʻzbekiston,UZ)", # Uzbek (Uzbekistan,UZ)
"vai": u"ꕙꔤ", # Vai
"vai-rLATN": u"Vai (LATN)", # Vai (LATN)
"vai-rLATN_LR": u"Vai (Laibhiya,LR)", # Vai (Liberia,LR)
"vai-rVAII": u"ꕙꔤ (VAII)", # Vai (VAII)
"vai-rVAII_LR": u"ꕙꔤ (ꕞꔤꔫꕩ,LR)", # Vai (Liberia,LR)
"vi": u"Tiếng Việt", # Vietnamese
"vi-rVN": u"Tiếng Việt (Việt Nam)", # Vietnamese (Vietnam)
"vun": u"Kyivunjo", # Vunjo
"vun-rTZ": u"Kyivunjo (Tanzania)", # Vunjo (Tanzania)
"xog": u"Olusoga", # Soga
"xog-rUG": u"Olusoga (Yuganda)", # Soga (Uganda)
"yav": u"Nuasue", # Yangben
"yav-rCM": u"Nuasue (Kemelún)", # Yangben (Cameroon)
"yo": u"Èdè Yorùbá", # Yoruba
"yo-rNG": u"Èdè Yorùbá (Orílẹ́ède Nàìjíríà)", # Yoruba (Nigeria)
# This was the obtained from Locale, but it seems it's different in Settings
#"zh": u"中文", # Chinese
"zh": u"中文 (简体)", # Chinese
"zh-rHANS": u"中文 (HANS)", # Chinese (HANS)
"zh-rHANS_CN": u"中文 (中国,CN)", # Chinese (China,CN)
"zh-rHANS_HK": u"中文 (香港,HK)", # Chinese (Hong Kong,HK)
"zh-rHANS_MO": u"中文 (澳门,MO)", # Chinese (Macau,MO)
"zh-rHANS_SG": u"中文 (新加坡,SG)", # Chinese (Singapore,SG)
"zh-rHANT": u"中文 (HANT)", # Chinese (HANT)
"zh-rHANT_HK": u"中文 (香港,HK)", # Chinese (Hong Kong,HK)
"zh-rHANT_MO": u"中文 (澳門,MO)", # Chinese (Macau,MO)
"zh-rHANT_TW": u"中文 (台灣,TW)", # Chinese (Taiwan,TW)
"zu": u"IsiZulu", # Zulu
"zu-rZA": u"IsiZulu (iNingizimu Afrika)", # Zulu (South Africa)
}
if not languageTo in LANGUAGES.keys():
raise RuntimeError("%s is not a supported language by AndroidViewClient" % languageTo)
self.openQuickSettingsSettings()
view = None
currentLanguage = None
ATTEMPTS = 10
if self.vc.getSdkVersion() >= 20:
for _ in range(ATTEMPTS):
com_android_settings___id_dashboard = self.vc.findViewByIdOrRaise("com.android.settings:id/dashboard")
for k, v in LANGUAGE_SETTINGS.iteritems():
if DEBUG_CHANGE_LANGUAGE:
print >> sys.stderr, "searching for", v
view = self.vc.findViewWithText(v, root=com_android_settings___id_dashboard)
if view:
currentLanguage = k
if DEBUG_CHANGE_LANGUAGE:
print >> sys.stderr, "found current language:", k
break
if view:
break
com_android_settings___id_dashboard.uiScrollable.flingForward()
self.vc.sleep(1)
self.vc.dump(-1)
if view is None:
raise ViewNotFoundException("text", "'Language & input' (any language)", "ROOT")
view.touch()
self.vc.sleep(1)
self.vc.dump(-1)
self.vc.findViewWithTextOrRaise(PHONE_LANGUAGE[currentLanguage]).touch()
self.vc.sleep(1)
self.vc.dump(-1)
else:
for _ in range(ATTEMPTS):
android___id_list = self.vc.findViewByIdOrRaise("android:id/list")
for k, v in LANGUAGE_SETTINGS.iteritems():
view = self.vc.findViewWithText(v, root=android___id_list)
if view:
currentLanguage = k
break
if view:
break
android___id_list.uiScrollable.flingForward()
self.vc.sleep(1)
self.vc.dump(-1)
if view is None:
raise ViewNotFoundException("text", "'Language & input' (any language)", "ROOT")
view.touch()
self.vc.sleep(1)
self.vc.dump(-1)
self.vc.findViewWithTextOrRaise(PHONE_LANGUAGE[currentLanguage]).touch()
self.vc.sleep(1)
self.vc.dump(-1)
android___id_list = self.vc.findViewByIdOrRaise("android:id/list")
android___id_list.uiScrollable.setViewClient(self.vc)
if DEBUG_CHANGE_LANGUAGE:
print >> sys.stderr, "scrolling to find", LANGUAGES[languageTo]
view = android___id_list.uiScrollable.scrollTextIntoView(LANGUAGES[languageTo])
if view is not None:
view.touch()
else:
#raise RuntimeError(u"Couldn't change language to %s (%s)" % (LANGUAGES[languageTo], languageTo))
raise RuntimeError("Couldn't change language to %s" % languageTo)
self.vc.device.press('BACK')
self.vc.sleep(1)
self.vc.device.press('BACK')
class UiCollection():
'''
Used to enumerate a container's user interface (UI) elements for the purpose of counting, or
targeting a sub elements by a child's text or description.
'''
pass
class UiScrollable(UiCollection):
'''
A L{UiCollection} that supports searching for items in scrollable layout elements.
This class can be used with horizontally or vertically scrollable controls.
'''
def __init__(self, view):
self.vc = None
self.view = view
self.vertical = True
self.bounds = view.getBounds()
(self.x, self.y, self.w, self.h) = view.getPositionAndSize()
self.steps = 10
self.duration = 500
self.swipeDeadZonePercentage = 0.1
self.maxSearchSwipes = 10
def flingBackward(self):
if self.vertical:
s = (self.x + self.w/2, self.y + self.h * self.swipeDeadZonePercentage)
e = (self.x + self.w/2, self.y + self.h - self.h * self.swipeDeadZonePercentage)
else:
s = (self.x + self.w * self.swipeDeadZonePercentage, self.y + self.h/2)
e = (self.x + self.w * (1.0 - self.swipeDeadZonePercentage), self.y + self.h/2)
if DEBUG:
print >> sys.stderr, "flingBackward: view=", self.view.__smallStr__(), self.view.getPositionAndSize()
print >> sys.stderr, "self.view.device.drag(%s, %s, %s, %s)" % (s, e, self.duration, self.steps)
self.view.device.drag(s, e, self.duration, self.steps, self.view.device.display['orientation'])
def flingForward(self):
if self.vertical:
s = (self.x + self.w/2, (self.y + self.h ) - self.h * self.swipeDeadZonePercentage)
e = (self.x + self.w/2, self.y + self.h * self.swipeDeadZonePercentage)
else:
s = (self.x + self.w * (1.0 - self.swipeDeadZonePercentage), self.y + self.h/2)
e = (self.x + self.w * self.swipeDeadZonePercentage, self.y + self.h/2)
if DEBUG:
print >> sys.stderr, "flingForward: view=", self.view.__smallStr__(), self.view.getPositionAndSize()
print >> sys.stderr, "self.view.device.drag(%s, %s, %s, %s)" % (s, e, self.duration, self.steps)
self.view.device.drag(s, e, self.duration, self.steps, self.view.device.display['orientation'])
def flingToBeginning(self, maxSwipes=10):
if self.vertical:
for _ in range(maxSwipes):
if DEBUG:
print >> sys.stderr, "flinging to beginning"
self.flingBackward()
def flingToEnd(self, maxSwipes=10):
if self.vertical:
for _ in range(maxSwipes):
if DEBUG:
print >> sys.stderr, "flinging to end"
self.flingForward()
def scrollTextIntoView(self, text):
'''
Performs a forward scroll action on the scrollable layout element until the text you provided is visible,
or until swipe attempts have been exhausted. See setMaxSearchSwipes(int)
'''
if self.vc is None:
raise ValueError('vc must be set in order to use this method')
for n in range(self.maxSearchSwipes):
# FIXME: now I need to figure out the best way of navigating to the ViewClient asossiated
# with this UiScrollable.
# It's using setViewClient() now.
if DEBUG or DEBUG_CHANGE_LANGUAGE:
print >> sys.stderr, u"Searching for text='%s'" % text
for v in self.vc.views:
try:
print >> sys.stderr, " scrollTextIntoView: v=", v.getId(),
print >> sys.stderr, v.getText()
except Exception, e:
print >> sys.stderr, e
pass
#v = self.vc.findViewWithText(text, root=self.view)
v = self.vc.findViewWithText(text)
if v is not None:
return v
self.flingForward()
#self.vc.sleep(1)
self.vc.dump(-1)
# WARNING: after this dump, the value kept in self.view is outdated, it should be refreshed
# in some way
return None
def setAsHorizontalList(self):
self.vertical = False
def setAsVerticalList(self):
self.vertical = True
def setMaxSearchSwipes(self, maxSwipes):
self.maxSearchSwipes = maxSwipes
def setViewClient(self, vc):
self.vc = vc
class ListView(View):
'''
ListView class.
'''
pass
class UiAutomator2AndroidViewClient():
'''
UiAutomator XML to AndroidViewClient
'''
def __init__(self, device, version, uiAutomatorHelper):
self.device = device
self.version = version
self.uiAutomatorHelper = uiAutomatorHelper
self.root = None
self.nodeStack = []
self.parent = None
self.views = []
self.idCount = 1
def StartElement(self, name, attributes):
'''
Expat start element event handler
'''
if name == 'hierarchy':
pass
elif name == 'node':
# Instantiate an Element object
attributes['uniqueId'] = 'id/no_id/%d' % self.idCount
bounds = re.split('[\][,]', attributes['bounds'])
attributes['bounds'] = ((int(bounds[1]), int(bounds[2])), (int(bounds[4]), int(bounds[5])))
if DEBUG_BOUNDS:
print >> sys.stderr, "bounds=", attributes['bounds']
self.idCount += 1
child = View.factory(attributes, self.device, version=self.version, uiAutomatorHelper=self.uiAutomatorHelper)
self.views.append(child)
# Push element onto the stack and make it a child of parent
if not self.nodeStack:
self.root = child
else:
self.parent = self.nodeStack[-1]
self.parent.add(child)
self.nodeStack.append(child)
def EndElement(self, name):
'''
Expat end element event handler
'''
if name == 'hierarchy':
pass
elif name == 'node':
self.nodeStack.pop()
def CharacterData(self, data):
'''
Expat character data event handler
'''
if data.strip():
data = data.encode()
element = self.nodeStack[-1]
element.cdata += data
def Parse(self, uiautomatorxml):
# Create an Expat parser
parser = xml.parsers.expat.ParserCreate() # @UndefinedVariable
# Set the Expat event handlers to our methods
parser.StartElementHandler = self.StartElement
parser.EndElementHandler = self.EndElement
parser.CharacterDataHandler = self.CharacterData
# Parse the XML File
try:
encoded = uiautomatorxml.encode(encoding='utf-8', errors='replace')
_ = parser.Parse(encoded, True)
except xml.parsers.expat.ExpatError, ex: # @UndefinedVariable
print >>sys.stderr, "ERROR: Offending XML:\n", repr(uiautomatorxml)
raise RuntimeError(ex)
return self.root
class Excerpt2Code():
''' Excerpt XML to code '''
def __init__(self):
self.data = None
def StartElement(self, name, attributes):
'''
Expat start element event handler
'''
if name == 'excerpt':
pass
else:
warnings.warn("Unexpected element: '%s'" % name)
def EndElement(self, name):
'''
Expat end element event handler
'''
if name == 'excerpt':
pass
def CharacterData(self, data):
'''
Expat character data event handler
'''
if data.strip():
data = data.encode()
if not self.data:
self.data = data
else:
self.data += data
def Parse(self, excerpt):
# Create an Expat parser
parser = xml.parsers.expat.ParserCreate() # @UndefinedVariable
# Set the Expat event handlers to our methods
parser.StartElementHandler = self.StartElement
parser.EndElementHandler = self.EndElement
parser.CharacterDataHandler = self.CharacterData
# Parse the XML
_ = parser.Parse(excerpt, 1)
return self.data
class ViewClientOptions:
'''
ViewClient options helper class
'''
DEVIDE = 'device'
SERIALNO = 'serialno'
AUTO_DUMP = 'autodump'
FORCE_VIEW_SERVER_USE = 'forceviewserveruse'
LOCAL_PORT = 'localport' # ViewServer local port
REMOTE_PORT = 'remoteport' # ViewServer remote port
START_VIEW_SERVER = 'startviewserver'
IGNORE_UIAUTOMATOR_KILLED = 'ignoreuiautomatorkilled'
COMPRESSED_DUMP = 'compresseddump'
USE_UIAUTOMATOR_HELPER = 'useuiautomatorhelper'
class ViewClient:
'''
ViewClient is a I{ViewServer} client.
ViewServer backend
==================
If not running the ViewServer is started on the target device or emulator and then the port
mapping is created.
LocalViewServer backend
=======================
ViewServer is started as an application services instead of as a system service.
UiAutomator backend
===================
No service is started.
null backend
============
Allows only operations using PX or DIP as hierarchy is not dumped and thus Views not recognized.
UiAutomatorHelper backend
=========================
Requires B{Culebra Tester} installed on Android device.
'''
imageDirectory = None
''' The directory used to store screenshot images '''
def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse=False, localport=VIEW_SERVER_PORT, remoteport=VIEW_SERVER_PORT, startviewserver=True, ignoreuiautomatorkilled=False, compresseddump=True, useuiautomatorhelper=False):
'''
Constructor
@type device: AdbClient
@param device: The device running the C{View server} to which this client will connect
@type serialno: str
@param serialno: the serial number of the device or emulator to connect to
@type adb: str
@param adb: the path of the C{adb} executable or None and C{ViewClient} will try to find it
@type autodump: boolean
@param autodump: whether an automatic dump is performed at the end of this constructor
@type forceviewserveruse: boolean
@param forceviewserveruse: Force the use of C{ViewServer} even if the conditions to use
C{UiAutomator} are satisfied
@type localport: int
@param localport: the local port used in the redirection
@type remoteport: int
@param remoteport: the remote port used to start the C{ViewServer} in the device or
emulator
@type startviewserver: boolean
@param startviewserver: Whether to start the B{global} ViewServer
@type ignoreuiautomatorkilled: boolean
@param ignoreuiautomatorkilled: Ignores received B{Killed} message from C{uiautomator}
@type compresseddump: boolean
@param compresseddump: turns --compressed flag for uiautomator dump on/off
@:type useuiautomatorhelper: boolean
@:param useuiautomatorhelper: use UiAutomatorHelper Android app as backend
'''
if not device:
raise Exception('Device is not connected')
self.device = device
''' The C{AdbClient} device instance '''
if not serialno:
raise ValueError("Serialno cannot be None")
self.serialno = self.__mapSerialNo(serialno)
''' The serial number of the device '''
self.uiAutomatorHelper = None
''' The UiAutomatorHelper '''
if DEBUG_DEVICE: print >> sys.stderr, "ViewClient: using device with serialno", self.serialno
if adb:
if not os.access(adb, os.X_OK):
raise Exception('adb="%s" is not executable' % adb)
else:
# Using adbclient we don't need adb executable yet (maybe it's needed if we want to
# start adb if not running)
adb = obtainAdbPath()
self.adb = adb
''' The adb command '''
self.root = None
''' The root node '''
self.viewsById = {}
''' The map containing all the L{View}s indexed by their L{View.getUniqueId()} '''
self.display = {}
''' The map containing the device's display properties: width, height and density '''
for prop in [ 'width', 'height', 'density', 'orientation' ]:
self.display[prop] = -1
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
try:
self.display[prop] = device.display[prop]
except:
if WARNINGS:
warnings.warn("Couldn't determine display %s" % prop)
else:
# these values are usually not defined as properties, so we stick to the -1 set
# before
pass
self.build = {}
''' The map containing the device's build properties: version.sdk, version.release '''
for prop in [VERSION_SDK_PROPERTY, VERSION_RELEASE_PROPERTY]:
self.build[prop] = -1
try:
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
self.build[prop] = device.getProperty(prop)
else:
self.build[prop] = device.shell('getprop ro.build.' + prop)[:-2]
except:
if WARNINGS:
warnings.warn("Couldn't determine build %s" % prop)
if prop == VERSION_SDK_PROPERTY:
# we expect it to be an int
self.build[prop] = int(self.build[prop] if self.build[prop] else -1)
self.ro = {}
''' The map containing the device's ro properties: secure, debuggable '''
for prop in ['secure', 'debuggable']:
try:
self.ro[prop] = device.shell('getprop ro.' + prop)[:-2]
except:
if WARNINGS:
warnings.warn("Couldn't determine ro %s" % prop)
self.ro[prop] = 'UNKNOWN'
self.forceViewServerUse = forceviewserveruse
''' Force the use of ViewServer even if the conditions to use UiAutomator are satisfied '''
self.useUiAutomator = (self.build[VERSION_SDK_PROPERTY] >= 16) and not forceviewserveruse # jelly bean 4.1 & 4.2
if DEBUG:
print >> sys.stderr, " ViewClient.__init__: useUiAutomator=", self.useUiAutomator, "sdk=", self.build[VERSION_SDK_PROPERTY], "forceviewserveruse=", forceviewserveruse
''' If UIAutomator is supported by the device it will be used '''
self.ignoreUiAutomatorKilled = ignoreuiautomatorkilled
''' On some devices (i.e. Nexus 7 running 4.2.2) uiautomator is killed just after generating
the dump file. In many cases the file is already complete so we can ask to ignore the 'Killed'
message by setting L{ignoreuiautomatorkilled} to C{True}.
Changes in v2.3.21 that uses C{/dev/tty} instead of a file may have turned this variable
unnecessary, however it has been kept for backward compatibility.
'''
if self.useUiAutomator:
self.textProperty = TEXT_PROPERTY_UI_AUTOMATOR
else:
if self.build[VERSION_SDK_PROPERTY] <= 10:
self.textProperty = TEXT_PROPERTY_API_10
else:
self.textProperty = TEXT_PROPERTY
if startviewserver:
if not self.serviceResponse(device.shell('service call window 3')):
try:
self.assertServiceResponse(device.shell('service call window 1 i32 %d' %
remoteport))
except:
msg = 'Cannot start View server.\n' \
'This only works on emulator and devices running developer versions.\n' \
'Does hierarchyviewer work on your device?\n' \
'See https://github.com/dtmilano/AndroidViewClient/wiki/Secure-mode\n\n' \
'Device properties:\n' \
' ro.secure=%s\n' \
' ro.debuggable=%s\n' % (self.ro['secure'], self.ro['debuggable'])
raise Exception(msg)
self.localPort = localport
self.remotePort = remoteport
# FIXME: it seems there's no way of obtaining the serialno from the MonkeyDevice
subprocess.check_call([self.adb, '-s', self.serialno, 'forward', 'tcp:%d' % self.localPort,
'tcp:%d' % self.remotePort])
self.windows = None
''' The list of windows as obtained by L{ViewClient.list()} '''
# FIXME: may not be true, one may want UiAutomator but without UiAutomatorHelper
if self.useUiAutomator:
if useuiautomatorhelper:
self.uiAutomatorHelper = UiAutomatorHelper(device)
else:
# culebratester Intrumentation running prevents `uiautomator dump` from working correctly, then if we are not
# using UiAutomatorHelper let's kill it, just in case
subprocess.check_call([self.adb, '-s', self.serialno, 'shell', 'am', 'force-stop', 'com.dtmilano.android.culebratester'])
self.uiDevice = UiDevice(self)
''' The L{UiDevice} '''
''' The output of compressed dump is different than output of uncompressed one.
If one requires uncompressed output, this option should be set to False
'''
self.compressedDump = compresseddump
self.navBack = None
self.navHome = None
self.navRecentApps = None
if autodump:
self.dump()
def __del__(self):
# should clean up some things
if hasattr(self, 'uiAutomatorHelper') and self.uiAutomatorHelper:
if DEBUG or True:
print >> sys.stderr, "Stopping UiAutomatorHelper..."
self.uiAutomatorHelper.quit()
@staticmethod
def __obtainAdbPath():
return obtainAdbPath()
@staticmethod
def __mapSerialNo(serialno):
serialno = serialno.strip()
#ipRE = re.compile('^\d+\.\d+.\d+.\d+$')
if IP_RE.match(serialno):
if DEBUG_DEVICE: print >>sys.stderr, "ViewClient: adding default port to serialno", serialno, ADB_DEFAULT_PORT
return serialno + ':%d' % ADB_DEFAULT_PORT
ipPortRE = re.compile('^\d+\.\d+.\d+.\d+:\d+$')
if ipPortRE.match(serialno):
# nothing to map
return serialno
if re.search("[.*()+]", serialno):
raise ValueError("Regular expression not supported as serialno in ViewClient. Found '%s'" % serialno)
return serialno
@staticmethod
def __obtainDeviceSerialNumber(device):
if DEBUG_DEVICE: print >>sys.stderr, "ViewClient: obtaining serial number for connected device"
serialno = device.getProperty('ro.serialno')
if not serialno:
serialno = device.shell('getprop ro.serialno')
if serialno:
serialno = serialno[:-2]
if not serialno:
qemu = device.shell('getprop ro.kernel.qemu')
if qemu:
qemu = qemu[:-2]
if qemu and int(qemu) == 1:
# FIXME !!!!!
# this must be calculated from somewhere, though using a fixed serialno for now
warnings.warn("Running on emulator but no serial number was specified then 'emulator-5554' is used")
serialno = 'emulator-5554'
if not serialno:
# If there's only one device connected get its serialno
adb = ViewClient.__obtainAdbPath()
if DEBUG_DEVICE: print >>sys.stderr, " using adb=%s" % adb
s = subprocess.Popen([adb, 'get-serialno'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={}).communicate()[0][:-1]
if s != 'unknown':
serialno = s
if DEBUG_DEVICE: print >>sys.stderr, " serialno=%s" % serialno
if not serialno:
warnings.warn("Couldn't obtain the serialno of the connected device")
return serialno
@staticmethod
def setAlarm(timeout):
osName = platform.system()
if osName.startswith('Windows'): # alarm is not implemented in Windows
return
signal.alarm(timeout)
@staticmethod
def connectToDeviceOrExit(timeout=60, verbose=False, ignoresecuredevice=False, ignoreversioncheck=False, serialno=None):
'''
Connects to a device which serial number is obtained from the script arguments if available
or using the default regex C{.*}.
If the connection is not successful the script exits.
History
-------
In MonkeyRunner times, this method was a way of overcoming one of its limitations.
L{MonkeyRunner.waitForConnection()} returns a L{MonkeyDevice} even if the connection failed.
Then, to detect this situation, C{device.wake()} is attempted and if it fails then it is
assumed the previous connection failed.
@type timeout: int
@param timeout: timeout for the connection
@type verbose: bool
@param verbose: Verbose output
@type ignoresecuredevice: bool
@param ignoresecuredevice: Ignores the check for a secure device
@type ignoreversioncheck: bool
@param ignoreversioncheck: Ignores the check for a supported ADB version
@type serialno: str
@param serialno: The device or emulator serial number
@return: the device and serialno used for the connection
'''
progname = os.path.basename(sys.argv[0])
if serialno is None:
# eat all the extra options the invoking script may have added
args = sys.argv
while len(args) > 1 and args[1][0] == '-':
args.pop(1)
serialno = args[1] if len(args) > 1 else \
os.environ['ANDROID_SERIAL'] if os.environ.has_key('ANDROID_SERIAL') \
else '.*'
if IP_RE.match(serialno):
# If matches an IP address format and port was not specified add the default
serialno += ':%d' % ADB_DEFAULT_PORT
if verbose:
print >> sys.stderr, 'Connecting to a device with serialno=%s with a timeout of %d secs...' % \
(serialno, timeout)
ViewClient.setAlarm(timeout+5)
# NOTE: timeout is used for 2 different timeouts, the one to set the alarm to timeout the connection with
# adb and the timeout used by adb (once connected) for the sockets
device = adbclient.AdbClient(serialno, ignoreversioncheck=ignoreversioncheck, timeout=timeout)
ViewClient.setAlarm(0)
if verbose:
print >> sys.stderr, 'Connected to device with serialno=%s' % serialno
secure = device.getSystemProperty('ro.secure')
debuggable = device.getSystemProperty('ro.debuggable')
versionProperty = device.getProperty(VERSION_SDK_PROPERTY)
if versionProperty:
version = int(versionProperty)
else:
if verbose:
print "Couldn't obtain device SDK version"
version = -1
# we are going to use UiAutomator for versions >= 16 that's why we ignore if the device
# is secure if this is true
if secure == '1' and debuggable == '0' and not ignoresecuredevice and version < 16:
print >> sys.stderr, "%s: ERROR: Device is secure, AndroidViewClient won't work." % progname
if verbose:
print >> sys.stderr, " secure=%s debuggable=%s version=%d ignoresecuredevice=%s" % \
(secure, debuggable, version, ignoresecuredevice)
sys.exit(2)
if re.search("[.*()+]", serialno) and not re.search("(\d{1,3}\.){3}\d{1,3}", serialno):
# if a regex was used we have to determine the serialno used
serialno = ViewClient.__obtainDeviceSerialNumber(device)
if verbose:
print >> sys.stderr, 'Actual device serialno=%s' % serialno
return device, serialno
@staticmethod
def traverseShowClassIdAndText(view, extraInfo=None, noExtraInfo=None, extraAction=None):
'''
Shows the View class, id and text if available.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@type extraInfo: method
@param extraInfo: the View method to add extra info
@type noExtraInfo: bool
@param noExtraInfo: Don't add extra info
@type extraAction: method
@param extraAction: An extra action to be invoked for every view
@return: the string containing class, id, and text if available
'''
try:
eis = ''
if extraInfo:
eis = extraInfo(view)
if not eis and noExtraInfo:
eis = noExtraInfo
if eis:
eis = ' {0}'.format(eis)
if extraAction:
extraAction(view)
_str = unicode(view.getClass())
_str += ' '
_str += '%s' % view.getId()
_str += ' '
_str += view.getText() if view.getText() else ''
if eis:
_str += eis
return _str
except Exception, e:
import traceback
return u'Exception in view=%s: %s:%s\n%s' % (view.__smallStr__(), sys.exc_info()[0].__name__, e, traceback.format_exc())
@staticmethod
def traverseShowClassIdTextAndUniqueId(view):
'''
Shows the View class, id, text if available and unique id.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available and unique Id
'''
return ViewClient.traverseShowClassIdAndText(view, View.getUniqueId)
@staticmethod
def traverseShowClassIdTextAndContentDescription(view):
'''
Shows the View class, id, text if available and content description.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available and the content description
'''
return ViewClient.traverseShowClassIdAndText(view, View.getContentDescription, 'NAF')
@staticmethod
def traverseShowClassIdTextAndTag(view):
'''
Shows the View class, id, text if available and tag.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available and tag
'''
return ViewClient.traverseShowClassIdAndText(view, View.getTag, None)
@staticmethod
def traverseShowClassIdTextContentDescriptionAndScreenshot(view):
'''
Shows the View class, id, text if available and unique id and takes the screenshot.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available and the content description
'''
return ViewClient.traverseShowClassIdAndText(view, View.getContentDescription, 'NAF', extraAction=ViewClient.writeViewImageToFileInDir)
@staticmethod
def traverseShowClassIdTextAndCenter(view):
'''
Shows the View class, id and text if available and center.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available
'''
return ViewClient.traverseShowClassIdAndText(view, View.getCenter)
@staticmethod
def traverseShowClassIdTextPositionAndSize(view):
'''
Shows the View class, id and text if available.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available
'''
return ViewClient.traverseShowClassIdAndText(view, View.getPositionAndSize)
@staticmethod
def traverseShowClassIdTextAndBounds(view):
'''
Shows the View class, id and text if available.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: the string containing class, id, and text if available plus
View bounds
'''
return ViewClient.traverseShowClassIdAndText(view, View.getBounds)
@staticmethod
def traverseTakeScreenshot(view):
'''
Don't show any any, just takes the screenshot.
This function can be used as a transform function to L{ViewClient.traverse()}
@type view: I{View}
@param view: the View
@return: None
'''
return ViewClient.writeViewImageToFileInDir(view)
# methods that can be used to transform ViewClient.traverse output
TRAVERSE_CIT = traverseShowClassIdAndText
''' An alias for L{traverseShowClassIdAndText(view)} '''
TRAVERSE_CITUI = traverseShowClassIdTextAndUniqueId
''' An alias for L{traverseShowClassIdTextAndUniqueId(view)} '''
TRAVERSE_CITCD = traverseShowClassIdTextAndContentDescription
''' An alias for L{traverseShowClassIdTextAndContentDescription(view)} '''
TRAVERSE_CITG = traverseShowClassIdTextAndTag
''' An alias for L{traverseShowClassIdTextAndTag(view)} '''
TRAVERSE_CITC = traverseShowClassIdTextAndCenter
''' An alias for L{traverseShowClassIdTextAndCenter(view)} '''
TRAVERSE_CITPS = traverseShowClassIdTextPositionAndSize
''' An alias for L{traverseShowClassIdTextPositionAndSize(view)} '''
TRAVERSE_CITB = traverseShowClassIdTextAndBounds
''' An alias for L{traverseShowClassIdTextAndBounds(view)} '''
TRAVERSE_CITCDS = traverseShowClassIdTextContentDescriptionAndScreenshot
''' An alias for L{traverseShowClassIdTextContentDescriptionAndScreenshot(view)} '''
TRAVERSE_S = traverseTakeScreenshot
''' An alias for L{traverseTakeScreenshot(view)} '''
@staticmethod
def sleep(secs=1.0):
'''
Sleeps for the specified number of seconds.
@type secs: float
@param secs: number of seconds
'''
time.sleep(secs)
def assertServiceResponse(self, response):
'''
Checks whether the response received from the server is correct or raises and Exception.
@type response: str
@param response: Response received from the server
@raise Exception: If the response received from the server is invalid
'''
if not self.serviceResponse(response):
raise Exception('Invalid response received from service.')
def serviceResponse(self, response):
'''
Checks the response received from the I{ViewServer}.
@return: C{True} if the response received matches L{PARCEL_TRUE}, C{False} otherwise
'''
PARCEL_TRUE = "Result: Parcel(00000000 00000001 '........')\r\n"
''' The TRUE response parcel '''
if DEBUG:
print >>sys.stderr, "serviceResponse: comparing '%s' vs Parcel(%s)" % (response, PARCEL_TRUE)
return response == PARCEL_TRUE
def setViews(self, received, windowId=None):
'''
Sets L{self.views} to the received value splitting it into lines.
@type received: str
@param received: the string received from the I{View Server}
'''
if not received or received == "":
raise ValueError("received is empty")
self.views = []
''' The list of Views represented as C{str} obtained after splitting it into lines after being received from the server. Done by L{self.setViews()}. '''
self.__parseTree(received.split("\n"), windowId)
if DEBUG:
print >>sys.stderr, "there are %d views in this dump" % len(self.views)
def setViewsFromUiAutomatorDump(self, received):
'''
Sets L{self.views} to the received value parsing the received XML.
@type received: str
@param received: the string received from the I{UI Automator}
'''
if not received or received == "":
raise ValueError("received is empty")
self.views = []
''' The list of Views represented as C{str} obtained after splitting it into lines after being received from the server. Done by L{self.setViews()}. '''
self.__parseTreeFromUiAutomatorDump(received)
if DEBUG:
print >>sys.stderr, "there are %d views in this dump" % len(self.views)
def __splitAttrs(self, strArgs):
'''
Splits the C{View} attributes in C{strArgs} and optionally adds the view id to the C{viewsById} list.
Unique Ids
==========
It is very common to find C{View}s having B{NO_ID} as the Id. This turns very difficult to
use L{self.findViewById()}. To help in this situation this method assigns B{unique Ids}.
The B{unique Ids} are generated using the pattern C{id/no_id/<number>} with C{<number>} starting
at 1.
@type strArgs: str
@param strArgs: the string containing the raw list of attributes and values
@return: Returns the attributes map.
'''
if self.useUiAutomator:
raise RuntimeError("This method is not compatible with UIAutomator")
# replace the spaces in text:mText to preserve them in later split
# they are translated back after the attribute matches
textRE = re.compile('%s=%s,' % (self.textProperty, _nd('len')))
m = textRE.search(strArgs)
if m:
__textStart = m.end()
__textLen = int(m.group('len'))
__textEnd = m.end() + __textLen
s1 = strArgs[__textStart:__textEnd]
s2 = s1.replace(' ', WS)
strArgs = strArgs.replace(s1, s2, 1)
idRE = re.compile("(?P<viewId>id/\S+)")
attrRE = re.compile('%s(?P<parens>\(\))?=%s,(?P<val>[^ ]*)' % (_ns('attr'), _nd('len')), flags=re.DOTALL)
hashRE = re.compile('%s@%s' % (_ns('class'), _nh('oid')))
attrs = {}
viewId = None
m = idRE.search(strArgs)
if m:
viewId = m.group('viewId')
if DEBUG:
print >>sys.stderr, "found view with id=%s" % viewId
for attr in strArgs.split():
m = attrRE.match(attr)
if m:
__attr = m.group('attr')
__parens = '()' if m.group('parens') else ''
__len = int(m.group('len'))
__val = m.group('val')
if WARNINGS and __len != len(__val):
warnings.warn("Invalid len: expected: %d found: %d s=%s e=%s" % (__len, len(__val), __val[:50], __val[-50:]))
if __attr == self.textProperty:
# restore spaces that have been replaced
__val = __val.replace(WS, ' ')
attrs[__attr + __parens] = __val
else:
m = hashRE.match(attr)
if m:
attrs['class'] = m.group('class')
attrs['oid'] = m.group('oid')
else:
if DEBUG:
print >>sys.stderr, attr, "doesn't match"
if True: # was assignViewById
if not viewId:
# If the view has NO_ID we are assigning a default id here (id/no_id) which is
# immediately incremented if another view with no id was found before to generate
# a unique id
viewId = "id/no_id/1"
if viewId in self.viewsById:
# sometimes the view ids are not unique, so let's generate a unique id here
i = 1
while True:
newId = re.sub('/\d+$', '', viewId) + '/%d' % i
if not newId in self.viewsById:
break
i += 1
viewId = newId
if DEBUG:
print >>sys.stderr, "adding viewById %s" % viewId
# We are assigning a new attribute to keep the original id preserved, which could have
# been NO_ID repeated multiple times
attrs['uniqueId'] = viewId
return attrs
def __parseTree(self, receivedLines, windowId=None):
'''
Parses the View tree contained in L{receivedLines}. The tree is created and the root node assigned to L{self.root}.
This method also assigns L{self.viewsById} values using L{View.getUniqueId} as the key.
@type receivedLines: str
@param receivedLines: the string received from B{View Server}
'''
self.root = None
self.viewsById = {}
self.views = []
parent = None
parents = []
treeLevel = -1
newLevel = -1
lastView = None
for v in receivedLines:
if v == '' or v == 'DONE' or v == 'DONE.':
break
attrs = self.__splitAttrs(v)
if not self.root:
if v[0] == ' ':
raise Exception("Unexpected root element starting with ' '.")
self.root = View.factory(attrs, self.device, self.build[VERSION_SDK_PROPERTY], self.forceViewServerUse, windowId, self.uiAutomatorHelper)
if DEBUG: self.root.raw = v
treeLevel = 0
newLevel = 0
lastView = self.root
parent = self.root
parents.append(parent)
else:
newLevel = (len(v) - len(v.lstrip()))
if newLevel == 0:
raise Exception("newLevel==0 treeLevel=%d but tree can have only one root, v=%s" % (treeLevel, v))
child = View.factory(attrs, self.device, self.build[VERSION_SDK_PROPERTY], self.forceViewServerUse, windowId, self.uiAutomatorHelper)
if DEBUG: child.raw = v
if newLevel == treeLevel:
parent.add(child)
lastView = child
elif newLevel > treeLevel:
if (newLevel - treeLevel) != 1:
raise Exception("newLevel jumps %d levels, v=%s" % ((newLevel-treeLevel), v))
parent = lastView
parents.append(parent)
parent.add(child)
lastView = child
treeLevel = newLevel
else: # newLevel < treeLevel
for _ in range(treeLevel - newLevel):
parents.pop()
parent = parents.pop()
parents.append(parent)
parent.add(child)
treeLevel = newLevel
lastView = child
self.views.append(lastView)
self.viewsById[lastView.getUniqueId()] = lastView
def __updateNavButtons(self):
"""
Updates the navigation buttons that might be on the device screen.
"""
navButtons = None
for v in self.views:
if v.getId() == 'com.android.systemui:id/nav_buttons':
navButtons = v
break
if navButtons:
self.navBack = self.findViewById('com.android.systemui:id/back', navButtons)
self.navHome = self.findViewById('com.android.systemui:id/home', navButtons)
self.navRecentApps = self.findViewById('com.android.systemui:id/recent_apps', navButtons)
else:
if self.uiAutomatorHelper:
print >> sys.stderr, "WARNING: nav buttons not found. Perhaps the device has hardware buttons."
self.navBack = None
self.navHome = None
self.navRecentApps = None
def __parseTreeFromUiAutomatorDump(self, receivedXml):
if DEBUG:
print >> sys.stderr, "__parseTreeFromUiAutomatorDump(", receivedXml[:40], "...)"
parser = UiAutomator2AndroidViewClient(self.device, self.build[VERSION_SDK_PROPERTY], self.uiAutomatorHelper)
try:
start_xml_index = receivedXml.index("<")
end_xml_index = receivedXml.rindex(">")
except ValueError:
raise ValueError("received does not contain valid XML: " + receivedXml)
self.root = parser.Parse(receivedXml[start_xml_index:end_xml_index+1])
self.views = parser.views
self.viewsById = {}
for v in self.views:
self.viewsById[v.getUniqueId()] = v
self.__updateNavButtons()
if DEBUG_NAV_BUTTONS:
if not self.navBack:
print >> sys.stderr, "WARNING: navBack not found"
if not self.navHome:
print >> sys.stderr, "WARNING: navHome not found"
if not self.navRecentApps:
print >> sys.stderr, "WARNING: navRecentApps not found"
def getRoot(self):
'''
Gets the root node of the C{View} tree
@return: the root node of the C{View} tree
'''
return self.root
def traverse(self, root="ROOT", indent="", transform=None, stream=sys.stdout):
'''
Traverses the C{View} tree and prints its nodes.
The nodes are printed converting them to string but other transformations can be specified
by providing a method name as the C{transform} parameter.
@type root: L{View}
@param root: the root node from where the traverse starts
@type indent: str
@param indent: the indentation string to use to print the nodes
@type transform: method
@param transform: a method to use to transform the node before is printed
'''
if transform is None:
# this cannot be a default value, otherwise
# TypeError: 'staticmethod' object is not callable
# is raised
transform = ViewClient.TRAVERSE_CIT
if type(root) == types.StringType and root == "ROOT":
root = self.root
return ViewClient.__traverse(root, indent, transform, stream)
# if not root:
# return
#
# s = transform(root)
# if s:
# print >>stream, "%s%s" % (indent, s)
#
# for ch in root.children:
# self.traverse(ch, indent=indent+" ", transform=transform, stream=stream)
@staticmethod
def __traverse(root, indent="", transform=View.__str__, stream=sys.stdout):
if not root:
return
s = transform(root)
if stream and s:
ius = "%s%s" % (indent, s if isinstance(s, unicode) else unicode(s, 'utf-8', 'replace'))
print >>stream, ius.encode('utf-8', 'replace')
for ch in root.children:
ViewClient.__traverse(ch, indent=indent+" ", transform=transform, stream=stream)
def dump(self, window=-1, sleep=1):
'''
Dumps the window content.
Sleep is useful to wait some time before obtaining the new content when something in the
window has changed.
@type window: int or str
@param window: the window id or name of the window to dump.
The B{name} is the package name or the window name (i.e. StatusBar) for
system windows.
The window id can be provided as C{int} or C{str}. The C{str} should represent
and C{int} in either base 10 or 16.
Use -1 to dump all windows.
This parameter only is used when the backend is B{ViewServer} and it's
ignored for B{UiAutomator}.
@type sleep: int
@param sleep: sleep in seconds before proceeding to dump the content
@return: the list of Views as C{str} received from the server after being split into lines
'''
if sleep > 0:
time.sleep(sleep)
if self.useUiAutomator:
if self.uiAutomatorHelper:
received = self.uiAutomatorHelper.dumpWindowHierarchy()
else:
api = self.getSdkVersion()
if api >= 23:
# In API 23 the process' stdout,in and err are connected to the socket not to the pts as in
# previous versions, so we can't redirect to /dev/tty
# Also, if we want to write to /sdcard/something it fails event though /sdcard is a symlink
if self.serialno.startswith('emulator'):
pathname = '/storage/self'
else:
pathname = '/sdcard'
filename = 'window_dump.xml'
cmd = 'uiautomator dump %s %s/%s >/dev/null && cat %s/%s' % ('--compressed' if self.compressedDump else '', pathname, filename, pathname, filename)
received = self.device.shell(cmd)
else:
# NOTICE:
# Using /dev/tty this works even on devices with no sdcard
received = self.device.shell('uiautomator dump %s /dev/tty >/dev/null' % ('--compressed' if api >= 18 and self.compressedDump else ''))
if received:
received = unicode(received, encoding='utf-8', errors='replace')
if not received:
raise RuntimeError('ERROR: Empty UiAutomator dump was received')
if DEBUG:
self.received = received
if DEBUG_RECEIVED:
print >>sys.stderr, "received %d chars" % len(received)
print >>sys.stderr
print >>sys.stderr, repr(received)
print >>sys.stderr
onlyKilledRE = re.compile('Killed$')
if onlyKilledRE.search(received):
MONKEY = 'com.android.commands.monkey'
extraInfo = ''
if self.device.shell('ps | grep "%s"' % MONKEY):
extraInfo = "\nIt is know that '%s' conflicts with 'uiautomator'. Please kill it and try again." % MONKEY
raise RuntimeError('''ERROR: UiAutomator output contains no valid information. UiAutomator was killed, no reason given.''' + extraInfo)
if self.ignoreUiAutomatorKilled:
if DEBUG_RECEIVED:
print >>sys.stderr, "ignoring UiAutomator Killed"
killedRE = re.compile('</hierarchy>[\n\S]*Killed', re.MULTILINE)
if killedRE.search(received):
received = re.sub(killedRE, '</hierarchy>', received)
elif DEBUG_RECEIVED:
print "UiAutomator Killed: NOT FOUND!"
# It seems that API18 uiautomator spits this message to stdout
dumpedToDevTtyRE = re.compile('</hierarchy>[\n\S]*UI hierchary dumped to: /dev/tty.*', re.MULTILINE)
if dumpedToDevTtyRE.search(received):
received = re.sub(dumpedToDevTtyRE, '</hierarchy>', received)
if DEBUG_RECEIVED:
print >>sys.stderr, "received=", received
# API19 seems to send this warning as part of the XML.
# Let's remove it if present
received = received.replace('WARNING: linker: libdvm.so has text relocations. This is wasting memory and is a security risk. Please fix.\r\n', '')
if re.search('\[: not found', received):
raise RuntimeError('''ERROR: Some emulator images (i.e. android 4.1.2 API 16 generic_x86) does not include the '[' command.
While UiAutomator back-end might be supported 'uiautomator' command fails.
You should force ViewServer back-end.''')
if received.startswith('ERROR: could not get idle state.'):
# See https://android.googlesource.com/platform/frameworks/testing/+/jb-mr2-release/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
raise RuntimeError('''The views are being refreshed too frequently to dump.''')
if received.find(u'Only ROTATION_0 supported') != -1:
raise RuntimeError('''UiAutomatorHelper backend with support for only ROTATION_0 found.''')
self.setViewsFromUiAutomatorDump(received)
else:
if isinstance(window, str):
if window != '-1':
self.list(sleep=0)
found = False
for wId in self.windows:
try:
if window == self.windows[wId]:
window = wId
found = True
break
except:
pass
try:
if int(window) == wId:
window = wId
found = True
break
except:
pass
try:
if int(window, 16) == wId:
window = wId
found = True
break
except:
pass
if not found:
raise RuntimeError("ERROR: Cannot find window '%s' in %s" % (window, self.windows))
else:
window = -1
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((VIEW_SERVER_HOST, self.localPort))
except socket.error, ex:
raise RuntimeError("ERROR: Connecting to %s:%d: %s" % (VIEW_SERVER_HOST, self.localPort, ex))
cmd = 'dump %x\r\n' % window
if DEBUG:
print >>sys.stderr, "executing: '%s'" % cmd
s.send(cmd)
received = ""
doneRE = re.compile("DONE")
ViewClient.setAlarm(120)
while True:
if DEBUG_RECEIVED:
print >>sys.stderr, " reading from socket..."
received += s.recv(1024)
if doneRE.search(received[-7:]):
break
s.close()
ViewClient.setAlarm(0)
if DEBUG:
self.received = received
if DEBUG_RECEIVED:
print >>sys.stderr, "received %d chars" % len(received)
print >>sys.stderr
print >>sys.stderr, received
print >>sys.stderr
if received:
for c in received:
if ord(c) > 127:
received = unicode(received, encoding='utf-8', errors='replace')
break
self.setViews(received, hex(window)[2:])
if DEBUG_TREE:
self.traverse(self.root)
return self.views
def list(self, sleep=1):
'''
List the windows.
Sleep is useful to wait some time before obtaining the new content when something in the
window has changed.
This also sets L{self.windows} as the list of windows.
@type sleep: int
@param sleep: sleep in seconds before proceeding to dump the content
@return: the list of windows
'''
if sleep > 0:
time.sleep(sleep)
if self.useUiAutomator:
raise Exception("Not implemented yet: listing windows with UiAutomator")
else:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((VIEW_SERVER_HOST, self.localPort))
except socket.error, ex:
raise RuntimeError("ERROR: Connecting to %s:%d: %s" % (VIEW_SERVER_HOST, self.localPort, ex))
s.send('list\r\n')
received = ""
doneRE = re.compile("DONE")
while True:
received += s.recv(1024)
if doneRE.search(received[-7:]):
break
s.close()
if DEBUG:
self.received = received
if DEBUG_RECEIVED:
print >>sys.stderr, "received %d chars" % len(received)
print >>sys.stderr
print >>sys.stderr, received
print >>sys.stderr
self.windows = {}
for line in received.split('\n'):
if not line:
break
if doneRE.search(line):
break
values = line.split()
if len(values) > 1:
package = values[1]
else:
package = "UNKNOWN"
if len(values) > 0:
wid = values[0]
else:
wid = '00000000'
self.windows[int('0x' + wid, 16)] = package
return self.windows
def findViewById(self, viewId, root="ROOT", viewFilter=None):
'''
Finds the View with the specified viewId.
@type viewId: str
@param viewId: the ID of the view to find
@type root: str
@type root: View
@param root: the root node of the tree where the View will be searched
@type: viewFilter: function
@param viewFilter: a function that will be invoked providing the candidate View as a parameter
and depending on the return value (C{True} or C{False}) the View will be
selected and returned as the result of C{findViewById()} or ignored.
This can be C{None} and no extra filtering is applied.
@return: the C{View} found or C{None}
'''
if not root:
return None
if type(root) == types.StringType and root == "ROOT":
return self.findViewById(viewId, self.root, viewFilter)
if root.getId() == viewId:
if viewFilter:
if viewFilter(root):
return root
else:
return root
if re.match('^id/no_id', viewId) or re.match('^id/.+/.+', viewId):
if root.getUniqueId() == viewId:
if viewFilter:
if viewFilter(root):
return root;
else:
return root
for ch in root.children:
foundView = self.findViewById(viewId, ch, viewFilter)
if foundView:
if viewFilter:
if viewFilter(foundView):
return foundView
else:
return foundView
def findViewByIdOrRaise(self, viewId, root="ROOT", viewFilter=None):
'''
Finds the View or raise a ViewNotFoundException.
@type viewId: str
@param viewId: the ID of the view to find
@type root: str
@type root: View
@param root: the root node of the tree where the View will be searched
@type: viewFilter: function
@param viewFilter: a function that will be invoked providing the candidate View as a parameter
and depending on the return value (C{True} or C{False}) the View will be
selected and returned as the result of C{findViewById()} or ignored.
This can be C{None} and no extra filtering is applied.
@return: the View found
@raise ViewNotFoundException: raise the exception if View not found
'''
view = self.findViewById(viewId, root, viewFilter)
if view:
return view
else:
raise ViewNotFoundException("ID", viewId, root)
def findViewByTag(self, tag, root="ROOT"):
'''
Finds the View with the specified tag
'''
return self.findViewWithAttribute('getTag()', tag, root)
def findViewByTagOrRaise(self, tag, root="ROOT"):
'''
Finds the View with the specified tag or raise a ViewNotFoundException
'''
view = self.findViewWithAttribute('getTag()', tag, root)
if view:
return view
else:
raise ViewNotFoundException("tag", tag, root)
def __findViewsWithAttributeInTree(self, attr, val, root):
# Note the plural in this method name
matchingViews = []
if not self.root:
print >>sys.stderr, "ERROR: no root, did you forget to call dump()?"
return matchingViews
if type(root) == types.StringType and root == "ROOT":
root = self.root
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: type val=", type(val)
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: checking if root=%s has attr=%s == %s" % (root.__smallStr__(), attr, val)
if root and attr in root.map and root.map[attr] == val:
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: FOUND: %s" % root.__smallStr__()
matchingViews.append(root)
else:
for ch in root.children:
matchingViews += self.__findViewsWithAttributeInTree(attr, val, ch)
return matchingViews
def __findViewWithAttributeInTree(self, attr, val, root):
if DEBUG:
print >> sys.stderr, " __findViewWithAttributeInTree: type(val)=", type(val)
if type(val) != types.UnicodeType and type(val) != re._pattern_type:
u = unicode(val, encoding='utf-8', errors='ignore')
else:
u = val
print >> sys.stderr, u'''__findViewWithAttributeInTree({0}'''.format(attr),
try:
print >> sys.stderr, u''', {0}'''.format(u),
except:
pass
print >> sys.stderr, u'>>>>>>>>>>>>>>>>>>', type(root)
if type(root) == types.StringType:
print >> sys.stderr, u'>>>>>>>>>>>>>>>>>>', root
print >> sys.stderr, u''', {0})'''.format(root)
else:
print >> sys.stderr, u''', {0})'''.format(root.__smallStr__())
if not self.root:
print >>sys.stderr, "ERROR: no root, did you forget to call dump()?"
return None
if type(root) == types.StringType and root == "ROOT":
root = self.root
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: type val=", type(val)
if DEBUG:
#print >> sys.stderr, u'''__findViewWithAttributeInTree: checking if root={0}: '''.format(root),
print >> sys.stderr, u'''has {0} == '''.format(attr),
if type(val) == types.UnicodeType:
u = val
elif type(val) != re._pattern_type:
u = unicode(val, encoding='utf-8', errors='replace')
try:
print >> sys.stderr, u'''{0}'''.format(u)
except:
pass
if isinstance(val, RegexType):
return self.__findViewWithAttributeInTreeThatMatches(attr, val, root)
else:
try:
if DEBUG:
print >> sys.stderr, u'''__findViewWithAttributeInTree: comparing {0}: '''.format(attr),
print >> sys.stderr, u'''{0} == '''.format(root.map[attr]),
print >> sys.stderr, u'''{0}'''.format(val)
except:
pass
if root and attr in root.map and root.map[attr] == val:
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: FOUND: %s" % root.__smallStr__()
return root
else:
for ch in root.children:
v = self.__findViewWithAttributeInTree(attr, val, ch)
if v:
return v
return None
def __findViewWithAttributeInTreeOrRaise(self, attr, val, root):
view = self.__findViewWithAttributeInTree(attr, val, root)
if view:
return view
else:
raise ViewNotFoundException(attr, val, root)
def __findViewWithAttributeInTreeThatMatches(self, attr, regex, root, rlist=[]):
if not self.root:
print >>sys.stderr, "ERROR: no root, did you forget to call dump()?"
return None
if type(root) == types.StringType and root == "ROOT":
root = self.root
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTreeThatMatches: checking if root=%s attr=%s matches %s" % (root.__smallStr__(), attr, regex)
if root and attr in root.map and regex.match(root.map[attr]):
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTreeThatMatches: FOUND: %s" % root.__smallStr__()
return root
#print >>sys.stderr, "appending root=%s to rlist=%s" % (root.__smallStr__(), rlist)
#return rlist.append(root)
else:
for ch in root.children:
v = self.__findViewWithAttributeInTreeThatMatches(attr, regex, ch, rlist)
if v:
return v
#print >>sys.stderr, "appending v=%s to rlist=%s" % (v.__smallStr__(), rlist)
#return rlist.append(v)
return None
#return rlist
def findViewWithAttribute(self, attr, val, root="ROOT"):
'''
Finds the View with the specified attribute and value
'''
if DEBUG:
try:
print >> sys.stderr, u'findViewWithAttribute({0}, {1}, {2})'.format(attr, unicode(val, encoding='utf-8', errors='replace'), root)
except:
pass
print >> sys.stderr, " findViewWithAttribute: type(val)=", type(val)
return self.__findViewWithAttributeInTree(attr, val, root)
def findViewsWithAttribute(self, attr, val, root="ROOT"):
'''
Finds the Views with the specified attribute and value.
This allows you to see all items that match your criteria in the view hierarchy
Usage:
buttons = v.findViewsWithAttribute("class", "android.widget.Button")
'''
return self.__findViewsWithAttributeInTree(attr, val, root)
def findViewWithAttributeOrRaise(self, attr, val, root="ROOT"):
'''
Finds the View or raise a ViewNotFoundException.
@return: the View found
@raise ViewNotFoundException: raise the exception if View not found
'''
view = self.findViewWithAttribute(attr, val, root)
if view:
return view
else:
raise ViewNotFoundException(attr, val, root)
def findViewWithAttributeThatMatches(self, attr, regex, root="ROOT"):
'''
Finds the list of Views with the specified attribute matching
regex
'''
return self.__findViewWithAttributeInTreeThatMatches(attr, regex, root)
def findViewWithText(self, text, root="ROOT"):
if DEBUG:
try:
print >>sys.stderr, '''findViewWithText({0}, {1})'''.format(text, root)
print >> sys.stderr, " findViewWithText: type(text)=", type(text)
except:
pass
if isinstance(text, RegexType):
return self.findViewWithAttributeThatMatches(self.textProperty, text, root)
#l = self.findViewWithAttributeThatMatches(TEXT_PROPERTY, text)
#ll = len(l)
#if ll == 0:
# return None
#elif ll == 1:
# return l[0]
#else:
# print >>sys.stderr, "WARNING: findViewWithAttributeThatMatches invoked by findViewWithText returns %d items." % ll
# return l
else:
return self.findViewWithAttribute(self.textProperty, text, root)
def findViewWithTextOrRaise(self, text, root="ROOT"):
'''
Finds the View or raise a ViewNotFoundException.
@return: the View found
@raise ViewNotFoundException: raise the exception if View not found
'''
if DEBUG:
print >>sys.stderr, "findViewWithTextOrRaise(%s, %s)" % (text, root)
view = self.findViewWithText(text, root)
if view:
return view
else:
raise ViewNotFoundException("text", text, root)
def findViewWithContentDescription(self, contentdescription, root="ROOT"):
'''
Finds the View with the specified content description
'''
return self.__findViewWithAttributeInTree('content-desc', contentdescription, root)
def findViewWithContentDescriptionOrRaise(self, contentdescription, root="ROOT"):
'''
Finds the View with the specified content description
'''
return self.__findViewWithAttributeInTreeOrRaise('content-desc', contentdescription, root)
def findViewsContainingPoint(self, (x, y), _filter=None):
'''
Finds the list of Views that contain the point (x, y).
'''
if not _filter:
_filter = lambda v: True
return [v for v in self.views if (v.containsPoint((x,y)) and _filter(v))]
def findObject(self, **kwargs):
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Finding object with %s through UiAutomatorHelper" % (kwargs)
return self.uiAutomatorHelper.findObject(**kwargs)
else:
warnings.warn("findObject only implemented using UiAutomatorHelper. Use ViewClient.findView...() instead.")
return None
def touch(self, x=-1, y=-1, selector=None):
if self.uiAutomatorHelper:
if selector:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Touching View by selector=%s through UiAutomatorHelper" % (selector)
self.uiAutomatorHelper.findObject(selector=selector).click()
else:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Touching (%d, %d) through UiAutomatorHelper" % (x, y)
self.uiAutomatorHelper.click(x=int(x), y=int(y))
else:
self.device.touch(x, y)
def longTouch(self, x=-1, y=-1, selector=None):
if self.uiAutomatorHelper:
if selector:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "ViewClient: Long-touching View by selector=%s through UiAutomatorHelper" % (selector)
self.uiAutomatorHelper.findObject(selector=selector).longClick()
else:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "ViewClient: Long-touching (%d, %d) through UiAutomatorHelper" % (x, y)
self.uiAutomatorHelper.swipe(startX=int(x), startY=int(y), endX=int(x), endY=int(y), steps=400)
else:
self.device.longTouch(x, y)
def swipe(self, x0=-1, y0=-1, x1=-1, y1=-1, steps=400, segments=[], segmentSteps=5):
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Swipe through UiAutomatorHelper", (x0, y0, x1, y1, steps, segments, segmentSteps)
self.uiAutomatorHelper.swipe(startX=x0, startY=y0, endX=x1, endY=y1, steps=steps, segments=segments, segmentSteps=segmentSteps)
else:
warnings.warn("swipe only implemented using UiAutomatorHelper. Use AdbClient.drag() instead.")
def pressBack(self):
if self.uiAutomatorHelper:
self.uiAutomatorHelper.pressBack()
else:
warnings.warn("pressBak only implemented using UiAutomatorHelper. Use AdbClient.type() instead")
def pressHome(self):
if self.uiAutomatorHelper:
self.uiAutomatorHelper.pressHome()
else:
warnings.warn("pressHome only implemented using UiAutomatorHelper. Use AdbClient.type() instead")
def pressRecentApps(self):
if self.uiAutomatorHelper:
self.uiAutomatorHelper.pressRecentApps()
else:
warnings.warn("pressRecentApps only implemented using UiAutomatorHelper. Use AdbClient.type() instead")
def pressKeyCode(self, keycode, metaState=0):
'''By default no meta state'''
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "pressKeyCode(%d, %d)" % (keycode, metaState)
self.uiAutomatorHelper.pressKeyCode(keycode, metaState)
else:
warnings.warn("pressKeyCode only implemented using UiAutomatorHelper. Use AdbClient.type() instead")
def setText(self, v, text):
if DEBUG:
print >> sys.stderr, "setText(%s, '%s')" % (v.__tinyStr__(), text)
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Setting text through UiAutomatorHelper for View with ID=%s" % v.getId()
if v.getId():
oid = self.uiAutomatorHelper.findObject(selector='res@%s' % v.getId())
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "oid=", oid, "text=", text
self.uiAutomatorHelper.setText(oid, text)
else:
# The View has no ID so we cannot use the ID to create a selector to find it using findObject()
# Let's fall back to this method.
v.setText(text)
else:
# 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)
def getViewIds(self):
'''
@deprecated: Use L{getViewsById} instead.
Returns the Views map.
'''
return self.viewsById
def getViewsById(self):
'''
Returns the Views map. The keys are C{uniqueIds} and the values are C{View}s.
'''
return self.viewsById
def __getFocusedWindowPosition(self):
return self.__getFocusedWindowId()
def getSdkVersion(self):
'''
Gets the SDK version.
'''
return self.build[VERSION_SDK_PROPERTY]
def isKeyboardShown(self):
'''
Whether the keyboard is displayed.
'''
return self.device.isKeyboardShown()
def writeImageToFile(self, filename, _format="PNG", deviceart=None, dropshadow=True, screenglare=True):
'''
Write the View image to the specified filename in the specified format.
@type filename: str
@param filename: Absolute path and optional filename receiving the image. If this points to
a directory, then the filename is determined by the serialno of the device and
format extension.
@type _format: str
@param _format: Image format (default format is PNG)
'''
filename = self.device.substituteDeviceTemplate(filename)
if not os.path.isabs(filename):
raise ValueError("writeImageToFile expects an absolute path (filename='%s')" % filename)
if os.path.isdir(filename):
filename = os.path.join(filename, self.serialno + '.' + _format.lower())
if DEBUG:
print >> sys.stderr, "writeImageToFile: saving image to '%s' in %s format (reconnect=%s)" % (filename, _format, self.device.reconnect)
if self.uiAutomatorHelper:
if DEBUG_UI_AUTOMATOR_HELPER:
print >> sys.stderr, "Taking screenshot using UiAutomatorHelper"
received = self.uiAutomatorHelper.takeScreenshot()
stream = StringIO.StringIO(received)
try:
from PIL import Image
image = Image.open(stream)
except ImportError as ex:
self.pilNotInstalledWarning()
sys.exit(1)
except IOError, ex:
print >> sys.stderr, ex
print repr(stream)
sys.exit(1)
else:
image = self.device.takeSnapshot(reconnect=self.device.reconnect)
if deviceart:
if 'STUDIO_DIR' in os.environ:
PLUGIN_DIR = 'plugins/android/lib/device-art-resources'
osName = platform.system()
if osName == 'Darwin':
deviceArtDir = os.environ['STUDIO_DIR'] + '/Contents/' + PLUGIN_DIR
else:
deviceArtDir = os.environ['STUDIO_DIR'] + '/' + PLUGIN_DIR
# FIXME: should parse XML
deviceArtXml = deviceArtDir + '/device-art.xml'
if not os.path.exists(deviceArtXml):
warnings.warn("Cannot find device art definition file")
# <device id="nexus_5" name="Nexus 5">
# <orientation name="port" size="1370,2405" screenPos="144,195" screenSize="1080,1920" shadow="port_shadow.png" back="port_back.png" lights="port_fore.png"/>
# <orientation name="land" size="2497,1235" screenPos="261,65" screenSize="1920,1080" shadow="land_shadow.png" back="land_back.png" lights="land_fore.png"/>
# </device>
orientation = self.display['orientation']
if orientation == 0 or orientation == 2:
orientationName = 'port'
elif orientation == 1 or orientation == 3:
orientationName = 'land'
else:
warnings.warn("Unknown orientation=" + orientation)
orientationName = 'port'
separator = '_'
if deviceart == 'auto':
hardware = self.device.getProperty('ro.hardware')
if hardware == 'hammerhead':
deviceart = 'nexus_5'
elif hardware == 'mako':
deviceart = 'nexus_4'
elif hardware == 'grouper':
deviceart = 'nexus_7' # 2012
elif hardware == 'flo':
deviceart = 'nexus_7_2013'
elif hardware in ['mt5861', 'mt5890']:
deviceart = 'tv_1080p'
elif hardware == 'universal5410':
deviceart = 'samsung_s4'
SUPPORTED_DEVICES = ['nexus_5', 'nexus_4', 'nexus_7', 'nexus_7_2013', 'tv_1080p', 'samsung_s4']
if deviceart not in SUPPORTED_DEVICES:
warnings.warn("Only %s is supported now, more devices coming soon" % SUPPORTED_DEVICES)
if deviceart == 'auto':
# it wasn't detected yet, let's assume generic phone
deviceart = 'phone'
screenSize = None
if deviceart == 'nexus_5':
if orientationName == 'port':
screenPos = (144, 195)
else:
screenPos = (261, 65)
elif deviceart == 'nexus_4':
if orientationName == 'port':
screenPos = (94, 187)
else:
screenPos = (257, 45)
elif deviceart == 'nexus_7': # 2012
if orientationName == 'port':
screenPos = (142, 190)
else:
screenPos = (260, 105)
elif deviceart == 'nexus_7_2013':
if orientationName == 'port':
screenPos = (130, 201)
screenSize = (800, 1280)
else:
screenPos = (282, 80)
screenSize = (1280, 800)
elif deviceart == 'tv_1080p':
screenPos = (85, 59)
orientationName = ''
separator = ''
elif deviceart == 'samsung_s4':
if orientationName == 'port':
screenPos = (76, 220)
screenSize = (1078, 1902) # FIXME: (1080, 1920) is the original size
else:
screenPos = (0, 0)
elif deviceart == 'phone':
if orientationName == 'port':
screenPos = (113, 93)
screenSize = (343, 46) # 46?, this is in device-art.xml
else:
screenPos = (141, 36)
screenSize = (324, 255)
deviceArtModelDir = deviceArtDir + '/' + deviceart
if not os.path.isdir(deviceArtModelDir):
warnings.warn("Cannot find device art for " + deviceart + ' at ' + deviceArtModelDir)
try:
from PIL import Image
if dropshadow:
dropShadowImage = Image.open(deviceArtModelDir + '/%s%sshadow.png' % (orientationName, separator))
deviceBack = Image.open(deviceArtModelDir + '/%s%sback.png' % (orientationName, separator))
if dropshadow:
dropShadowImage.paste(deviceBack, (0, 0), deviceBack)
deviceBack = dropShadowImage
if screenSize:
image = image.resize(screenSize, Image.ANTIALIAS)
deviceBack.paste(image, screenPos)
if screenglare:
screenGlareImage = Image.open(deviceArtModelDir + '/%s%sfore.png' % (orientationName, separator))
deviceBack.paste(screenGlareImage, (0, 0), screenGlareImage)
image = deviceBack
except ImportError as ex:
self.pilNotInstalledWarning()
else:
warnings.warn("ViewClient.writeImageToFile: Cannot add device art because STUDIO_DIR environment variable was not set")
image.save(filename, _format)
def pilNotInstalledWarning(self):
warnings.warn('''PIL or Pillow is needed for image manipulation
On Ubuntu install
$ sudo apt-get install python-imaging python-imaging-tk
On OSX install
$ brew install homebrew/python/pillow
''')
def installPackage(self, apk):
return subprocess.check_call([self.adb, "install", "-r", apk], shell=False)
@staticmethod
def writeViewImageToFileInDir(view):
'''
Write the View image to the directory specified in C{ViewClient.imageDirectory}.
@type view: View
@param view: The view
'''
if not ViewClient.imageDirectory:
raise RuntimeError('You must set ViewClient.imageDiretory in order to use this method')
view.writeImageToFile(ViewClient.imageDirectory)
@staticmethod
def __pickleable(tree):
'''
Makes the tree pickleable.
'''
def removeDeviceReference(view):
'''
Removes the reference to a L{MonkeyDevice}.
'''
view.device = None
###########################################################################################
# FIXME: Unfortunately deepcopy does not work with MonkeyDevice objects, which is
# sadly the reason why we cannot pickle the tree and we need to remove the MonkeyDevice
# references.
# We wanted to copy the tree to preserve the original and make piclkleable the copy.
#treeCopy = copy.deepcopy(tree)
treeCopy = tree
# IMPORTANT:
# This assumes that the first element in the list is the tree root
ViewClient.__traverse(treeCopy[0], transform=removeDeviceReference)
###########################################################################################
return treeCopy
def distanceTo(self, tree):
'''
Calculates the distance between the current state and the tree passed as argument.
@type tree: list of Views
@param tree: Tree of Views
@return: the distance
'''
return ViewClient.distance(ViewClient.__pickleable(self.views), tree)
@staticmethod
def distance(tree1, tree2):
'''
Calculates the distance between the two trees.
@type tree1: list of Views
@param tree1: Tree of Views
@type tree2: list of Views
@param tree2: Tree of Views
@return: the distance
'''
################################################################
#FIXME: this should copy the entire tree and then transform it #
################################################################
pickleableTree1 = ViewClient.__pickleable(tree1)
pickleableTree2 = ViewClient.__pickleable(tree2)
s1 = pickle.dumps(pickleableTree1)
s2 = pickle.dumps(pickleableTree2)
if DEBUG_DISTANCE:
print >>sys.stderr, "distance: calculating distance between", s1[:20], "and", s2[:20]
l1 = len(s1)
l2 = len(s2)
t = float(max(l1, l2))
if l1 == l2:
if DEBUG_DISTANCE:
print >>sys.stderr, "distance: trees have same length, using Hamming distance"
return ViewClient.__hammingDistance(s1, s2)/t
else:
if DEBUG_DISTANCE:
print >>sys.stderr, "distance: trees have different length, using Levenshtein distance"
return ViewClient.__levenshteinDistance(s1, s2)/t
@staticmethod
def __hammingDistance(s1, s2):
'''
Finds the Hamming distance between two strings.
@param s1: string
@param s2: string
@return: the distance
@raise ValueError: if the lenght of the strings differ
'''
l1 = len(s1)
l2 = len(s2)
if l1 != l2:
raise ValueError("Hamming distance requires strings of same size.")
return sum(ch1 != ch2 for ch1, ch2 in zip(s1, s2))
def hammingDistance(self, tree):
'''
Finds the Hamming distance between this tree and the one passed as argument.
'''
s1 = ' '.join(map(View.__str__, self.views))
s2 = ' '.join(map(View.__str__, tree))
return ViewClient.__hammingDistance(s1, s2)
@staticmethod
def __levenshteinDistance(s, t):
'''
Find the Levenshtein distance between two Strings.
Python version of Levenshtein distance method implemented in Java at
U{http://www.java2s.com/Code/Java/Data-Type/FindtheLevenshteindistancebetweentwoStrings.htm}.
This is the number of changes needed to change one String into
another, where each change is a single character modification (deletion,
insertion or substitution).
The previous implementation of the Levenshtein distance algorithm
was from U{http://www.merriampark.com/ld.htm}
Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError
which can occur when my Java implementation is used with very large strings.
This implementation of the Levenshtein distance algorithm
is from U{http://www.merriampark.com/ldjava.htm}::
StringUtils.getLevenshteinDistance(null, *) = IllegalArgumentException
StringUtils.getLevenshteinDistance(*, null) = IllegalArgumentException
StringUtils.getLevenshteinDistance("","") = 0
StringUtils.getLevenshteinDistance("","a") = 1
StringUtils.getLevenshteinDistance("aaapppp", "") = 7
StringUtils.getLevenshteinDistance("frog", "fog") = 1
StringUtils.getLevenshteinDistance("fly", "ant") = 3
StringUtils.getLevenshteinDistance("elephant", "hippo") = 7
StringUtils.getLevenshteinDistance("hippo", "elephant") = 7
StringUtils.getLevenshteinDistance("hippo", "zzzzzzzz") = 8
StringUtils.getLevenshteinDistance("hello", "hallo") = 1
@param s: the first String, must not be null
@param t: the second String, must not be null
@return: result distance
@raise ValueError: if either String input C{null}
'''
if s is None or t is None:
raise ValueError("Strings must not be null")
n = len(s)
m = len(t)
if n == 0:
return m
elif m == 0:
return n
if n > m:
tmp = s
s = t
t = tmp
n = m;
m = len(t)
p = [None]*(n+1)
d = [None]*(n+1)
for i in range(0, n+1):
p[i] = i
for j in range(1, m+1):
if DEBUG_DISTANCE:
if j % 100 == 0:
print >>sys.stderr, "DEBUG:", int(j/(m+1.0)*100),"%\r",
t_j = t[j-1]
d[0] = j
for i in range(1, n+1):
cost = 0 if s[i-1] == t_j else 1
# minimum of cell to the left+1, to the top+1, diagonally left and up +cost
d[i] = min(min(d[i-1]+1, p[i]+1), p[i-1]+cost)
_d = p
p = d
d = _d
if DEBUG_DISTANCE:
print >> sys.stderr, "\n"
return p[n]
def levenshteinDistance(self, tree):
'''
Finds the Levenshtein distance between this tree and the one passed as argument.
'''
s1 = ' '.join(map(View.__microStr__, self.views))
s2 = ' '.join(map(View.__microStr__, tree))
return ViewClient.__levenshteinDistance(s1, s2)
@staticmethod
def excerpt(_str, execute=False):
code = Excerpt2Code().Parse(_str)
if execute:
exec code
else:
return code
class ConnectedDevice:
def __init__(self, device, vc, serialno):
self.device = device
self.vc = vc
self.serialno = serialno
class CulebraOptions:
'''
Culebra options helper class
'''
HELP = 'help'
VERBOSE = 'verbose'
VERSION = 'version'
IGNORE_SECURE_DEVICE = 'ignore-secure-device'
IGNORE_VERSION_CHECK = 'ignore-version-check'
FORCE_VIEW_SERVER_USE = 'force-view-server-use' # Same a ViewClientOptions.FORCE_VIEW_SERVER_USE but with dashes
DO_NOT_START_VIEW_SERVER = 'do-not-start-view-server'
DO_NOT_IGNORE_UIAUTOMATOR_KILLED = 'do-not-ignore-uiautomator-killed'
FIND_VIEWS_BY_ID = 'find-views-by-id'
FIND_VIEWS_WITH_TEXT = 'find-views-with-text'
FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 'find-views-with-content-description'
USE_REGEXPS = 'use-regexps'
VERBOSE_COMMENTS = 'verbose-comments'
UNIT_TEST_CLASS = 'unit-test-class'
UNIT_TEST_METHOD = 'unit-test-method'
USE_JAR = 'use-jar'
USE_DICTIONARY = 'use-dictionary'
DICTIONARY_KEYS_FROM = 'dictionary-keys-from'
AUTO_REGEXPS = 'auto-regexps'
START_ACTIVITY = 'start-activity'
OUTPUT = 'output'
INTERACTIVE = 'interactive'
WINDOW = 'window'
APPEND_TO_SYS_PATH = 'append-to-sys-path'
PREPEND_TO_SYS_PATH = 'prepend-to-sys-path'
SAVE_SCREENSHOT = 'save-screenshot'
SAVE_VIEW_SCREENSHOTS = 'save-view-screenshots'
GUI = 'gui'
SCALE = 'scale'
DO_NOT_VERIFY_SCREEN_DUMP = 'do-not-verify-screen-dump'
ORIENTATION_LOCKED = 'orientation-locked'
SERIALNO = 'serialno'
MULTI_DEVICE = 'multi-device'
LOG_ACTIONS = 'log-actions'
DEVICE_ART = 'device-art'
DROP_SHADOW = 'drop-shadow'
SCREEN_GLARE = 'glare'
NULL_BACK_END = 'null-back-end'
USE_UIAUTOMATOR_HELPER = 'use-uiautomator-helper'
CONCERTINA = 'concertina'
INSTALL_APK = 'install-apk'
SHORT_OPTS = 'HVvIEFSkw:i:t:d:rCUM:j:D:K:R:a:o:pf:W:GuP:Os:mLA:ZB0hc1:'
LONG_OPTS = [HELP, VERBOSE, VERSION, IGNORE_SECURE_DEVICE, IGNORE_VERSION_CHECK, FORCE_VIEW_SERVER_USE,
DO_NOT_START_VIEW_SERVER,
DO_NOT_IGNORE_UIAUTOMATOR_KILLED,
WINDOW + '=',
FIND_VIEWS_BY_ID + '=', FIND_VIEWS_WITH_TEXT + '=', FIND_VIEWS_WITH_CONTENT_DESCRIPTION + '=',
USE_REGEXPS, VERBOSE_COMMENTS, UNIT_TEST_CLASS, UNIT_TEST_METHOD + '=',
USE_JAR + '=', USE_DICTIONARY + '=', DICTIONARY_KEYS_FROM + '=', AUTO_REGEXPS + '=',
START_ACTIVITY + '=',
OUTPUT + '=', PREPEND_TO_SYS_PATH,
SAVE_SCREENSHOT + '=', SAVE_VIEW_SCREENSHOTS + '=',
GUI,
DO_NOT_VERIFY_SCREEN_DUMP,
SCALE + '=',
ORIENTATION_LOCKED,
SERIALNO + '=',
MULTI_DEVICE,
LOG_ACTIONS,
DEVICE_ART + '=', DROP_SHADOW, SCREEN_GLARE,
NULL_BACK_END,
USE_UIAUTOMATOR_HELPER,
CONCERTINA,
INSTALL_APK + '=',
]
LONG_OPTS_ARG = {WINDOW: 'WINDOW',
FIND_VIEWS_BY_ID: 'BOOL', FIND_VIEWS_WITH_TEXT: 'BOOL', FIND_VIEWS_WITH_CONTENT_DESCRIPTION: 'BOOL',
USE_JAR: 'BOOL', USE_DICTIONARY: 'BOOL', DICTIONARY_KEYS_FROM: 'VALUE', AUTO_REGEXPS: 'LIST',
START_ACTIVITY: 'COMPONENT',
OUTPUT: 'FILENAME',
SAVE_SCREENSHOT: 'FILENAME', SAVE_VIEW_SCREENSHOTS: 'DIR',
UNIT_TEST_METHOD: 'NAME',
SCALE: 'FLOAT',
SERIALNO: 'LIST',
DEVICE_ART: 'MODEL',
INSTALL_APK: 'FILENAME'}
OPTS_HELP = {
'H': 'prints this help',
'V': 'verbose comments',
'v': 'prints version number and exists',
'k': 'don\'t ignore UiAutomator killed',
'w': 'use WINDOW content (default: -1, all windows)',
'i': 'whether to use findViewById() in script',
't': 'whether to use findViewWithText() in script',
'd': 'whether to use findViewWithContentDescription',
'r': 'use regexps in matches',
'U': 'generates unit test class and script',
'M': 'generates unit test method. Can be used with or without -U',
'j': 'use jar and appropriate shebang to run script (deprecated)',
'D': 'use a dictionary to store the Views found',
'K': 'dictionary keys from: id, text, content-description',
'R': 'auto regexps (i.e. clock), implies -r. help list options',
'a': 'starts Activity before dump',
'o': 'output filename',
'p': 'prepend environment variables values to sys.path',
'f': 'save screenshot to file',
'W': 'save View screenshots to files in directory',
'E': 'ignores ADB version check',
'G': 'presents the GUI (EXPERIMENTAL)',
'P': 'scale percentage (i.e. 0.5)',
'u': 'do not verify screen state after dump',
'O': 'orientation locked in generated test',
's': 'device serial number (can be more than 1)',
'm': 'enables multi-device test generation',
'L': 'log actions using logcat',
'A': 'device art model to frame screenshot (auto: autodetected)',
'Z': 'drop shadow for device art screenshot',
'B': 'screen glare over screenshot',
'0': 'use a null back-end (no View tree obtained)',
'h': 'use UiAutomatorHelper',
'c': 'enable concertina mode (EXPERIMENTAL)',
'1': 'install APK as precondition',
}
class CulebraTestCase(unittest.TestCase):
'''
The base class for all CulebraTests.
Class variables
---------------
There are some class variables that can be used to change the behavior of the tests.
B{serialno}: The serial number of the device. This can also be a list of devices for I{mutli-devices}
tests or the keyword C{all} to run the tests on all available devices or C{default} to run the tests
only on the default (first) device.
When a I{multi-device} test is running the available devices are available in a list named
L{self.devices} which has the corresponding L{ConnectedDevices} entries.
Also, in the case of I{multi-devices} tests and to be backward compatible with I{single-device} tests
the default device, the first one in the devices list, is assigned to L{self.device}, L{self.vc} and
L{self.serialno} too.
B{verbose}: The verbosity of the tests. This can be changed from the test command line using the
command line option C{-v} or C{--verbose}.
'''
kwargs1 = None
kwargs2 = None
devices = None
''' The list of connected devices '''
globalDevices = []
''' The list of connected devices (class instance) '''
defaultDevice = None
''' The default L{ConnectedDevice}. Set to the first one found for multi-device cases '''
serialno = None
''' The default connected device C{serialno} '''
device = None
''' The default connected device '''
vc = None
''' The default connected device C{ViewClient} '''
verbose = False
options = {}
@classmethod
def setUpClass(cls):
cls.kwargs1 = {'ignoreversioncheck': False, 'verbose': False, 'ignoresecuredevice': False}
cls.kwargs2 = {'startviewserver': True, 'forceviewserveruse': False, 'autodump': False, 'ignoreuiautomatorkilled': True}
@classmethod
def tearDownClass(cls):
if cls.kwargs2['useuiautomatorhelper']:
for d in cls.globalDevices:
d.vc.uiAutomatorHelper.quit()
def __init__(self, methodName='runTest'):
self.Log = CulebraTestCase.__Log(self)
unittest.TestCase.__init__(self, methodName=methodName)
def setUp(self):
__devices = None
if self.serialno:
# serialno can be 1 serialno, multiple serialnos, 'all' or 'default'
if self.serialno.lower() == 'all':
__devices = [d.serialno for d in adbclient.AdbClient().getDevices()]
elif self.serialno.lower() == 'default':
__devices = [adbclient.AdbClient().getDevices()[0].serialno]
else:
__devices = self.serialno.split()
if len(__devices) > 1:
self.devices = __devices
# FIXME: both cases should be unified
if self.devices:
__devices = self.devices
self.devices = []
for serialno in __devices:
device, serialno = ViewClient.connectToDeviceOrExit(serialno=serialno, **self.kwargs1)
if self.options[CulebraOptions.START_ACTIVITY]:
device.startActivity(component=self.options[CulebraOptions.START_ACTIVITY])
vc = ViewClient(device, serialno, **self.kwargs2)
connectedDevice = ConnectedDevice(serialno=serialno, device=device, vc=vc)
self.devices.append(connectedDevice)
CulebraTestCase.globalDevices.append(connectedDevice)
# Select the first devices as default
self.defaultDevice = self.devices[0]
self.device = self.defaultDevice.device
self.serialno = self.defaultDevice.serialno
self.vc = self.defaultDevice.vc
else:
self.devices = []
if __devices:
# A list containing only one device was specified
self.serialno = __devices[0]
self.device, self.serialno = ViewClient.connectToDeviceOrExit(serialno=self.serialno, **self.kwargs1)
if self.options[CulebraOptions.START_ACTIVITY]:
self.device.startActivity(component=self.options[CulebraOptions.START_ACTIVITY])
self.vc = ViewClient(self.device, self.serialno, **self.kwargs2)
# Set the default device, to be consistent with multi-devices case
connectedDevice = ConnectedDevice(serialno=self.serialno, device=self.device, vc=self.vc)
self.devices.append(connectedDevice)
CulebraTestCase.globalDevices.append(connectedDevice)
def tearDown(self):
pass
def preconditions(self):
if self.options[CulebraOptions.ORIENTATION_LOCKED] is not None:
# If orientation locked was set to a valid orientation value then use it to compare
# against current orientation (when the test is run)
return (self.device.display['orientation'] == self.options[CulebraOptions.ORIENTATION_LOCKED])
return True
def isTestRunningOnMultipleDevices(self):
return (len(self.devices) > 1)
@staticmethod
def __passAll(arg):
return True
def all(self, arg, _filter=None):
# CulebraTestCase.__passAll cannot be specified as the default argument value
if _filter is None:
_filter = CulebraTestCase.__passAll
if DEBUG_MULTI:
print >> sys.stderr, "all(%s, %s)" % (arg, _filter)
l = (getattr(d, arg) for d in self.devices)
for i in l:
print >> sys.stderr, " i=", i
return filter(_filter, (getattr(d, arg) for d in self.devices))
def allVcs(self, _filter=None):
return self.all('vc', _filter)
def allDevices(self, _filter=None):
return self.all('device', _filter)
def allSerialnos(self, _filter=None):
return self.all('serialno', _filter)
def log(self, message, priority='D'):
'''
Logs a message with the specified priority.
'''
self.device.log('CULEBRA', message, priority, CulebraTestCase.verbose)
class __Log():
'''
Log class to simulate C{android.util.Log}
'''
def __init__(self, culebraTestCase):
self.culebraTestCase = culebraTestCase
def __getattr__(self, attr):
'''
Returns the corresponding log method or @C{AttributeError}.
'''
if attr in ['v', 'd', 'i', 'w', 'e']:
return lambda message: self.culebraTestCase.log(message, priority=attr.upper())
raise AttributeError(self.__class__.__name__ + ' has no attribute "%s"' % attr)
@staticmethod
def main():
# If you want to specify tests classes and methods in the command line you will be forced
# to include -s or --serialno and the serial number of the device (could be a regexp)
# as ViewClient would have no way of determine what it is.
# This could be also a list of devices (delimited by whitespaces) and in such case all of
# them will be used.
# The special argument 'all' means all the connected devices.
ser = ['-s', '--serialno']
old = '%(failfast)'
new = ' %s s The serial number[s] to connect to or \'all\'\n%s' % (', '.join(ser), old)
unittest.TestProgram.USAGE = unittest.TestProgram.USAGE.replace(old, new)
argsToRemove = []
i = 0
while i < len(sys.argv):
a = sys.argv[i]
if a in ['-v', '--verbose']:
# make CulebraTestCase.verbose the same as unittest verbose
CulebraTestCase.verbose = True
elif a in ser:
# remove arguments not handled by unittest
if len(sys.argv) > (i+1):
argsToRemove.append(sys.argv[i])
CulebraTestCase.serialno = sys.argv[i+1]
argsToRemove.append(CulebraTestCase.serialno)
i += 1
else:
raise RuntimeError('serial number missing')
i += 1
for a in argsToRemove:
sys.argv.remove(a)
unittest.main()
if __name__ == "__main__":
try:
vc = ViewClient(None)
except:
print "%s: Don't expect this to do anything" % __file__