Skip to content

Commit fd1a465

Browse files
committed
AndroidViewClient is now 100% python
- Version 4.0.0 - Kissed monkeyrunner goodbye - Module private methods renamed (_) - Added VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED set to False - Improved UTF-8 support for XML - culebra: USE_JAR default value changed to False
1 parent 34b4da1 commit fd1a465

File tree

6 files changed

+162
-122
lines changed

6 files changed

+162
-122
lines changed

AndroidViewClient/examples/dump-simple.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#! /usr/bin/env monkeyrunner
1+
#! /usr/bin/env python
22
'''
33
Copyright (C) 2012 Diego Torres Milano
44
Created on Apr 30, 2013
@@ -10,8 +10,6 @@
1010
import sys
1111
import os
1212

13-
# This must be imported before MonkeyRunner and MonkeyDevice,
14-
# otherwise the import fails.
1513
# PyDev sets PYTHONPATH, use it
1614
try:
1715
for p in os.environ['PYTHONPATH'].split(':'):
@@ -27,4 +25,4 @@
2725

2826
from com.dtmilano.android.viewclient import ViewClient
2927

30-
ViewClient(*ViewClient.connectToDeviceOrExit()).traverse(transform=ViewClient.TRAVERSE_CIT)
28+
ViewClient(*ViewClient.connectToDeviceOrExit(verbose=True)).traverse(transform=ViewClient.TRAVERSE_CIT)

AndroidViewClient/src/com/dtmilano/android/viewclient.py

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@
1818
@author: Diego Torres Milano
1919
'''
2020

21-
__version__ = '3.2.0'
21+
__version__ = '4.0.0'
2222

2323
import sys
2424
import subprocess
2525
import re
2626
import socket
2727
import os
28-
import java
2928
import types
3029
import time
3130
import signal
3231
import warnings
3332
import copy
3433
import pickle
34+
import platform
3535
import xml.parsers.expat
36-
import org.python.modules.sre.PatternObject
37-
from com.android.monkeyrunner import MonkeyDevice, MonkeyRunner
36+
from com.dtmilano.android.adb import adbclient
3837

3938
DEBUG = False
4039
DEBUG_DEVICE = DEBUG and False
@@ -60,17 +59,22 @@
6059
''' This assumes the smallest touchable view on the screen is approximately 50px x 50px
6160
and touches it at M{(x+OFFSET, y+OFFSET)} '''
6261

63-
USE_MONKEYRUNNER_TO_GET_BUILD_PROPERTIES = True
64-
''' Use monkeyrunner (C{MonkeyDevice.getProperty()}) to obtain the needed properties. If this is
62+
USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES = True
63+
''' Use C{AdbClient} to obtain the needed properties. If this is
6564
C{False} then C{adb shell getprop} is used '''
6665

6766
SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED = False
6867
''' Skips some classes related with the Action Bar and the PhoneWindow$DecorView in the
6968
coordinates calculation
7069
@see: L{View.getXY()} '''
7170

71+
VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED = False
72+
''' Under some conditions the touch event should be longer [t(DOWN) << t(UP)]. C{True} enables a
73+
workaround to delay the events.'''
74+
7275
# some device properties
73-
VERSION_SDK_PROPERTY = 'version.sdk'
76+
VERSION_SDK_PROPERTY = 'ro.build.version.sdk'
77+
VERSION_RELEASE_PROPERTY = 'ro.build.version.release'
7478

7579
# some constants for the attributes
7680
ID_PROPERTY = 'mID'
@@ -95,22 +99,23 @@
9599
INVISIBLE = 0x4
96100
GONE = 0x8
97101

102+
RegexType = type(re.compile(''))
98103
IP_RE = re.compile('^(\d{1,3}\.){3}\d{1,3}$')
99104
ID_RE = re.compile('id/([^/]*)(/(\d+))?')
100105

101-
def __nd(name):
106+
def _nd(name):
102107
'''
103108
@return: Returns a named decimal regex
104109
'''
105110
return '(?P<%s>\d+)' % name
106111

107-
def __nh(name):
112+
def _nh(name):
108113
'''
109114
@return: Returns a named hex regex
110115
'''
111116
return '(?P<%s>[0-9a-f]+)' % name
112117

113-
def __ns(name, greedy=False):
118+
def _ns(name, greedy=False):
114119
'''
115120
NOTICE: this is using a non-greedy (or minimal) regex
116121
@@ -179,7 +184,7 @@ class ViewNotFoundException(Exception):
179184
'''
180185

181186
def __init__(self, attr, value, root):
182-
if isinstance(value, org.python.modules.sre.PatternObject):
187+
if isinstance(value, RegexType):
183188
msg = "Couldn't find View with %s that matches '%s' in tree with root=%s" % (attr, value.pattern, root)
184189
else:
185190
msg = "Couldn't find View with %s='%s' in tree with root=%s" % (attr, value, root)
@@ -242,8 +247,8 @@ def __init__(self, map, device, version=-1, forceviewserveruse=False):
242247
self.build[VERSION_SDK_PROPERTY] = version
243248
else:
244249
try:
245-
if USE_MONKEYRUNNER_TO_GET_BUILD_PROPERTIES:
246-
self.build[VERSION_SDK_PROPERTY] = int(device.getProperty('build.' + VERSION_SDK_PROPERTY))
250+
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
251+
self.build[VERSION_SDK_PROPERTY] = int(device.getProperty(VERSION_SDK_PROPERTY))
247252
else:
248253
self.build[VERSION_SDK_PROPERTY] = int(device.shell('getprop ro.build.' + VERSION_SDK_PROPERTY)[:-2])
249254
except:
@@ -689,20 +694,20 @@ def __dumpWindowsInformation(self, debug=False):
689694
if DEBUG_WINDOWS or debug: print >> sys.stderr, dww
690695
lines = dww.split('\n')
691696
widRE = re.compile('^ *Window #%s Window{%s (u\d+ )?%s?.*}:' %
692-
(__nd('num'), __nh('winId'), __ns('activity', greedy=True)))
693-
currentFocusRE = re.compile('^ mCurrentFocus=Window{%s .*' % __nh('winId'))
694-
viewVisibilityRE = re.compile(' mViewVisibility=0x%s ' % __nh('visibility'))
697+
(_nd('num'), _nh('winId'), _ns('activity', greedy=True)))
698+
currentFocusRE = re.compile('^ mCurrentFocus=Window{%s .*' % _nh('winId'))
699+
viewVisibilityRE = re.compile(' mViewVisibility=0x%s ' % _nh('visibility'))
695700
# This is for 4.0.4 API-15
696701
containingFrameRE = re.compile('^ *mContainingFrame=\[%s,%s\]\[%s,%s\] mParentFrame=\[%s,%s\]\[%s,%s\]' %
697-
(__nd('cx'), __nd('cy'), __nd('cw'), __nd('ch'), __nd('px'), __nd('py'), __nd('pw'), __nd('ph')))
702+
(_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
698703
contentFrameRE = re.compile('^ *mContentFrame=\[%s,%s\]\[%s,%s\] mVisibleFrame=\[%s,%s\]\[%s,%s\]' %
699-
(__nd('x'), __nd('y'), __nd('w'), __nd('h'), __nd('vx'), __nd('vy'), __nd('vx1'), __nd('vy1')))
704+
(_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
700705
# This is for 4.1 API-16
701706
framesRE = re.compile('^ *Frames: containing=\[%s,%s\]\[%s,%s\] parent=\[%s,%s\]\[%s,%s\]' %
702-
(__nd('cx'), __nd('cy'), __nd('cw'), __nd('ch'), __nd('px'), __nd('py'), __nd('pw'), __nd('ph')))
707+
(_nd('cx'), _nd('cy'), _nd('cw'), _nd('ch'), _nd('px'), _nd('py'), _nd('pw'), _nd('ph')))
703708
contentRE = re.compile('^ *content=\[%s,%s\]\[%s,%s\] visible=\[%s,%s\]\[%s,%s\]' %
704-
(__nd('x'), __nd('y'), __nd('w'), __nd('h'), __nd('vx'), __nd('vy'), __nd('vx1'), __nd('vy1')))
705-
policyVisibilityRE = re.compile('mPolicyVisibility=%s ' % __ns('policyVisibility', greedy=True))
709+
(_nd('x'), _nd('y'), _nd('w'), _nd('h'), _nd('vx'), _nd('vy'), _nd('vx1'), _nd('vy1')))
710+
policyVisibilityRE = re.compile('mPolicyVisibility=%s ' % _ns('policyVisibility', greedy=True))
706711

707712
for l in range(len(lines)):
708713
m = widRE.search(lines[l])
@@ -783,20 +788,20 @@ def __dumpWindowsInformation(self, debug=False):
783788
if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: (0,0)"
784789
return (0,0)
785790

786-
def touch(self, type=MonkeyDevice.DOWN_AND_UP):
791+
def touch(self, type=adbclient.DOWN_AND_UP):
787792
'''
788793
Touches the center of this C{View}
789794
'''
790795

791796
(x, y) = self.getCenter()
792797
if DEBUG_TOUCH:
793798
print >>sys.stderr, "should touch @ (%d, %d)" % (x, y)
794-
if type == MonkeyDevice.DOWN_AND_UP:
799+
if VIEW_CLIENT_TOUCH_WORKAROUND_ENABLED and type == adbclient.DOWN_AND_UP:
795800
if WARNINGS:
796801
print >> sys.stderr, "ViewClient: touch workaround enabled"
797-
self.device.touch(x, y, MonkeyDevice.DOWN)
802+
self.device.touch(x, y, adbclient.DOWN)
798803
time.sleep(50/1000.0)
799-
self.device.touch(x+10, y+10, MonkeyDevice.UP)
804+
self.device.touch(x+10, y+10, adbclient.UP)
800805
else:
801806
self.device.touch(x, y, type)
802807

@@ -897,7 +902,12 @@ def __str__(self):
897902
if "class" in self.map:
898903
__str += " class=" + self.map["class"].__str__() + " "
899904
for a in self.map:
900-
__str += a + "=" + unicode(self.map[a], 'utf-8', 'replace') + " "
905+
__str += a + "="
906+
if isinstance(self.map[a], unicode):
907+
__str += self.map[a].encode('utf-8', 'replace')
908+
else:
909+
__str += unicode(str(self.map[a]), 'utf-8', 'replace')
910+
__str += " "
901911
__str += "] parent="
902912
if self.parent:
903913
if "class" in self.parent.map:
@@ -923,18 +933,18 @@ class EditText(TextView):
923933

924934
def type(self, text):
925935
self.touch()
926-
MonkeyRunner.sleep(1)
936+
time.sleep(1)
927937
for c in text:
928938
if c != ' ':
929939
self.device.type(c)
930940
else:
931-
self.device.press('KEYCODE_SPACE', MonkeyDevice.DOWN_AND_UP)
932-
MonkeyRunner.sleep(1)
941+
self.device.press('KEYCODE_SPACE', adbclient.DOWN_AND_UP)
942+
time.sleep(1)
933943

934944
def backspace(self):
935945
self.touch()
936-
MonkeyRunner.sleep(1)
937-
self.device.press('KEYCODE_DEL', MonkeyDevice.DOWN_AND_UP)
946+
time.sleep(1)
947+
self.device.press('KEYCODE_DEL', adbclient.DOWN_AND_UP)
938948

939949
class UiAutomator2AndroidViewClient():
940950
'''
@@ -1003,7 +1013,7 @@ def Parse(self, uiautomatorxml):
10031013
parser.CharacterDataHandler = self.CharacterData
10041014
# Parse the XML File
10051015
try:
1006-
parserStatus = parser.Parse(uiautomatorxml, 1)
1016+
parserStatus = parser.Parse(uiautomatorxml.encode(encoding='utf-8', errors='replace'), True)
10071017
except xml.parsers.expat.ExpatError, ex:
10081018
print >>sys.stderr, "ERROR: Offending XML:\n", repr(uiautomatorxml)
10091019
raise RuntimeError(ex)
@@ -1111,7 +1121,10 @@ def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse
11111121
if not os.access(adb, os.X_OK):
11121122
raise Exception('adb="%s" is not executable' % adb)
11131123
else:
1114-
adb = ViewClient.__obtainAdbPath()
1124+
# Using adbclient we don't need adb executable yet (maybe it's needed if we want to
1125+
# start adb if not running)
1126+
#adb = ViewClient.__obtainAdbPath()
1127+
adb = 'ADBCLIENT'
11151128

11161129
self.adb = adb
11171130
''' The adb command '''
@@ -1123,7 +1136,7 @@ def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse
11231136
''' The map containing the device's display properties: width, height and density '''
11241137
for prop in [ 'width', 'height', 'density' ]:
11251138
self.display[prop] = -1
1126-
if USE_MONKEYRUNNER_TO_GET_BUILD_PROPERTIES:
1139+
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
11271140
try:
11281141
self.display[prop] = int(device.getProperty('display.' + prop))
11291142
except:
@@ -1136,11 +1149,12 @@ def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse
11361149

11371150
self.build = {}
11381151
''' The map containing the device's build properties: version.sdk, version.release '''
1139-
for prop in [VERSION_SDK_PROPERTY, 'version.release']:
1152+
1153+
for prop in [VERSION_SDK_PROPERTY, VERSION_RELEASE_PROPERTY]:
11401154
self.build[prop] = -1
11411155
try:
1142-
if USE_MONKEYRUNNER_TO_GET_BUILD_PROPERTIES:
1143-
self.build[prop] = device.getProperty('build.' + prop)
1156+
if USE_ADB_CLIENT_TO_GET_BUILD_PROPERTIES:
1157+
self.build[prop] = device.getProperty(prop)
11441158
else:
11451159
self.build[prop] = device.shell('getprop ro.build.' + prop)[:-2]
11461160
except:
@@ -1164,14 +1178,16 @@ def __init__(self, device, serialno, adb=None, autodump=True, forceviewserveruse
11641178
self.forceViewServerUse = forceviewserveruse
11651179
''' Force the use of ViewServer even if the conditions to use UiAutomator are satisfied '''
11661180
self.useUiAutomator = (self.build[VERSION_SDK_PROPERTY] >= 16) and not forceviewserveruse # jelly bean 4.1 & 4.2
1181+
if DEBUG:
1182+
print >> sys.stderr, " ViewClient.__init__: useUiAutomator=", self.useUiAutomator, "sdk=", self.build[VERSION_SDK_PROPERTY], "forceviewserveruse=", forceviewserveruse
11671183
''' If UIAutomator is supported by the device it will be used '''
11681184
self.ignoreUiAutomatorKilled = ignoreuiautomatorkilled
11691185
''' On some devices (i.e. Nexus 7 running 4.2.2) uiautomator is killed just after generating
11701186
the dump file. In many cases the file is already complete so we can ask to ignore the 'Killed'
11711187
message by setting L{ignoreuiautomatorkilled} to C{True}.
11721188
1173-
Changes in 2.3.21 that uses C{/dev/tty} instead of a file may have turned this variable
1174-
unnnecessary, however it has been kept for backward compatibility.
1189+
Changes in v2.3.21 that uses C{/dev/tty} instead of a file may have turned this variable
1190+
unnecessary, however it has been kept for backward compatibility.
11751191
'''
11761192

11771193
if self.useUiAutomator:
@@ -1218,7 +1234,7 @@ def __obtainAdbPath():
12181234
Obtains the ADB path attempting know locations for different OSs
12191235
'''
12201236

1221-
osName = java.lang.System.getProperty('os.name')
1237+
osName = platform.system()
12221238
isWindows = False
12231239
if osName.startswith('Windows'):
12241240
adb = 'adb.exe'
@@ -1312,7 +1328,7 @@ def __obtainDeviceSerialNumber(device):
13121328

13131329
@staticmethod
13141330
def setAlarm(timeout):
1315-
osName = java.lang.System.getProperty('os.name')
1331+
osName = platform.system()
13161332
if osName.startswith('Windows'): # alarm is not implemented in Windows
13171333
return
13181334
signal.alarm(timeout)
@@ -1355,21 +1371,14 @@ def connectToDeviceOrExit(timeout=60, verbose=False, ignoresecuredevice=False, s
13551371
if verbose:
13561372
print >> sys.stderr, 'Connecting to a device with serialno=%s with a timeout of %d secs...' % \
13571373
(serialno, timeout)
1358-
# Sometimes MonkeyRunner doesn't even timeout (i.e. two connections from same process), so let's
1359-
# handle it here
13601374
ViewClient.setAlarm(timeout+5)
1361-
device = MonkeyRunner.waitForConnection(timeout, serialno)
1375+
device = adbclient.AdbClient(serialno)
13621376
ViewClient.setAlarm(0)
1363-
try:
1364-
device.wake()
1365-
except java.lang.NullPointerException, e:
1366-
print >> sys.stderr, "%s: ERROR: Couldn't connect to %s: %s" % (progname, serialno, e)
1367-
sys.exit(3)
13681377
if verbose:
13691378
print >> sys.stderr, 'Connected to device with serialno=%s' % serialno
13701379
secure = device.getSystemProperty('ro.secure')
13711380
debuggable = device.getSystemProperty('ro.debuggable')
1372-
versionProperty = device.getProperty('build.' + VERSION_SDK_PROPERTY)
1381+
versionProperty = device.getProperty(VERSION_SDK_PROPERTY)
13731382
if versionProperty:
13741383
version = int(versionProperty)
13751384
else:
@@ -1575,7 +1584,7 @@ def __splitAttrs(self, strArgs):
15751584
raise RuntimeError("This method is not compatible with UIAutomator")
15761585
# replace the spaces in text:mText to preserve them in later split
15771586
# they are translated back after the attribute matches
1578-
textRE = re.compile('%s=%s,' % (self.textProperty, __nd('len')))
1587+
textRE = re.compile('%s=%s,' % (self.textProperty, _nd('len')))
15791588
m = textRE.search(strArgs)
15801589
if m:
15811590
__textStart = m.end()
@@ -1586,8 +1595,8 @@ def __splitAttrs(self, strArgs):
15861595
strArgs = strArgs.replace(s1, s2, 1)
15871596

15881597
idRE = re.compile("(?P<viewId>id/\S+)")
1589-
attrRE = re.compile('%s(?P<parens>\(\))?=%s,(?P<val>[^ ]*)' % (__ns('attr'), __nd('len')), flags=re.DOTALL)
1590-
hashRE = re.compile('%s@%s' % (__ns('class'), __nh('oid')))
1598+
attrRE = re.compile('%s(?P<parens>\(\))?=%s,(?P<val>[^ ]*)' % (_ns('attr'), _nd('len')), flags=re.DOTALL)
1599+
hashRE = re.compile('%s@%s' % (_ns('class'), _nh('oid')))
15911600

15921601
attrs = {}
15931602
viewId = None
@@ -1783,15 +1792,14 @@ def dump(self, window=-1, sleep=1):
17831792
'''
17841793

17851794
if sleep > 0:
1786-
MonkeyRunner.sleep(sleep)
1795+
time.sleep(sleep)
17871796

17881797
if self.useUiAutomator:
17891798
# NOTICE:
17901799
# Using /dev/tty this works even on devices with no sdcard
1791-
received = self.device.shell('uiautomator dump /dev/tty >/dev/null')
1800+
received = unicode(self.device.shell('uiautomator dump /dev/tty >/dev/null'), encoding='utf-8', errors='replace')
17921801
if not received:
17931802
raise RuntimeError('ERROR: Empty UiAutomator dump was received')
1794-
received = received.encode('utf-8', 'ignore')
17951803
if DEBUG:
17961804
self.received = received
17971805
if DEBUG_RECEIVED:
@@ -1900,7 +1908,7 @@ def list(self, sleep=1):
19001908
'''
19011909

19021910
if sleep > 0:
1903-
MonkeyRunner.sleep(sleep)
1911+
time.sleep(sleep)
19041912

19051913
if self.useUiAutomator:
19061914
raise Exception("Not implemented yet: listing windows with UiAutomator")
@@ -2039,7 +2047,7 @@ def __findViewWithAttributeInTree(self, attr, val, root):
20392047
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: type val=", type(val)
20402048
if DEBUG: print >>sys.stderr, "__findViewWithAttributeInTree: checking if root=%s has attr=%s == %s" % (root.__smallStr__(), attr, val)
20412049

2042-
if isinstance(val, org.python.modules.sre.PatternObject):
2050+
if isinstance(val, RegexType):
20432051
return self.__findViewWithAttributeInTreeThatMatches(attr, val, root)
20442052
else:
20452053
if root and attr in root.map and root.map[attr] == val:
@@ -2118,7 +2126,7 @@ def findViewWithAttributeThatMatches(self, attr, regex, root="ROOT"):
21182126
def findViewWithText(self, text, root="ROOT"):
21192127
if DEBUG:
21202128
print >>sys.stderr, "findViewWithText(%s, %s)" % (text, root)
2121-
if isinstance(text, org.python.modules.sre.PatternObject):
2129+
if isinstance(text, RegexType):
21222130
return self.findViewWithAttributeThatMatches(self.textProperty, text, root)
21232131
#l = self.findViewWithAttributeThatMatches(TEXT_PROPERTY, text)
21242132
#ll = len(l)

0 commit comments

Comments
 (0)