Skip to content

Commit 4dc39ae

Browse files
committed
Parse UiAutomator XML
- Added enough properties to satisfy dump.py (from examples) using UiAutomator back end - Added UiAutomator2AndroidViewClient parser
1 parent 8ec7421 commit 4dc39ae

1 file changed

Lines changed: 122 additions & 31 deletions

File tree

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

Lines changed: 122 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
import time
3030
import signal
3131
import warnings
32+
import xml.parsers.expat
3233
from com.android.monkeyrunner import MonkeyDevice, MonkeyRunner
3334

3435
DEBUG = True
35-
DEBUG_DEVICE = DEBUG and True
36-
DEBUG_RECEIVED = DEBUG and True
36+
DEBUG_DEVICE = DEBUG and False
37+
DEBUG_RECEIVED = DEBUG and False
3738
DEBUG_TREE = DEBUG and False
3839
DEBUG_GETATTR = DEBUG and False
39-
DEBUG_COORDS = DEBUG and False
40+
DEBUG_COORDS = DEBUG and True
4041
DEBUG_TOUCH = DEBUG and False
4142
DEBUG_STATUSBAR = DEBUG and False
4243
DEBUG_WINDOWS = DEBUG and False
@@ -64,6 +65,7 @@
6465
VERSION_SDK_PROPERTY = 'version.sdk'
6566

6667
# some constants for the attributes
68+
ID_PROPERTY = 'mID'
6769
TEXT_PROPERTY = 'text:mText'
6870
TEXT_PROPERTY_API_10 = 'mText'
6971
WS = "\xfe" # the whitespace replacement char for TEXT_PROPERTY
@@ -284,7 +286,7 @@ def getId(self):
284286
'''
285287

286288
try:
287-
return self.map['mID']
289+
return self.map[ID_PROPERTY]
288290
except:
289291
return None
290292

@@ -304,16 +306,22 @@ def getText(self):
304306
return None
305307

306308
def getHeight(self):
307-
try:
308-
return int(self.map['layout:getHeight()'])
309-
except:
310-
return 0
309+
if USE_UI_AUTOMATOR:
310+
return self.map['bounds'][1][1]
311+
else:
312+
try:
313+
return int(self.map['layout:getHeight()'])
314+
except:
315+
return 0
311316

312317
def getWidth(self):
313-
try:
314-
return int(self.map['layout:getWidth()'])
315-
except:
316-
return 0
318+
if USE_UI_AUTOMATOR:
319+
return self.map['bounds'][1][0]
320+
else:
321+
try:
322+
return int(self.map['layout:getWidth()'])
323+
except:
324+
return 0
317325

318326
def getUniqueId(self):
319327
'''
@@ -352,12 +360,16 @@ def getX(self):
352360
if DEBUG_COORDS:
353361
print >>sys.stderr, "getX(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
354362
x = 0
355-
try:
356-
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
357-
if DEBUG_COORDS: print >>sys.stderr, " getX: VISIBLE adding %d" % int(self.map['layout:mLeft'])
358-
x += int(self.map['layout:mLeft'])
359-
except:
360-
warnings.warn("View %s has no 'layout:mLeft' property" % self.getId())
363+
364+
if USE_UI_AUTOMATOR:
365+
x = self.map['bounds'][0][0]
366+
else:
367+
try:
368+
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
369+
if DEBUG_COORDS: print >>sys.stderr, " getX: VISIBLE adding %d" % int(self.map['layout:mLeft'])
370+
x += int(self.map['layout:mLeft'])
371+
except:
372+
warnings.warn("View %s has no 'layout:mLeft' property" % self.getId())
361373

362374
if DEBUG_COORDS: print >>sys.stderr, " getX: returning %d" % (x)
363375
return x
@@ -370,12 +382,16 @@ def getY(self):
370382
if DEBUG_COORDS:
371383
print >>sys.stderr, "getY(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
372384
y = 0
373-
try:
374-
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
375-
if DEBUG_COORDS: print >>sys.stderr, " getY: VISIBLE adding %d" % int(self.map['layout:mTop'])
376-
y += int(self.map['layout:mTop'])
377-
except:
378-
warnings.warn("View %s has no 'layout:mTop' property" % self.getId())
385+
386+
if USE_UI_AUTOMATOR:
387+
y = self.map['bounds'][0][1]
388+
else:
389+
try:
390+
if GET_VISIBILITY_PROPERTY in self.map and self.map[GET_VISIBILITY_PROPERTY] == 'VISIBLE':
391+
if DEBUG_COORDS: print >>sys.stderr, " getY: VISIBLE adding %d" % int(self.map['layout:mTop'])
392+
y += int(self.map['layout:mTop'])
393+
except:
394+
warnings.warn("View %s has no 'layout:mTop' property" % self.getId())
379395

380396
if DEBUG_COORDS: print >>sys.stderr, " getY: returning %d" % (y)
381397
return y
@@ -557,7 +573,7 @@ def __dumpWindowsInformation(self):
557573
if m:
558574
visibility = int(m.group('visibility'))
559575
if DEBUG_COORDS: print >> sys.stderr, "__dumpWindowsInformation: visibility=", visibility
560-
if self.build[VERSION_SDK_PROPERTY] == 16:
576+
if self.build[VERSION_SDK_PROPERTY] >= 16:
561577
m = framesRE.search(lines[l2])
562578
if m:
563579
px, py = self.__obtainPxPy(m)
@@ -699,6 +715,73 @@ def type(self, text):
699715
self.device.type(text)
700716
MonkeyRunner.sleep(1)
701717

718+
class UiAutomator2AndroidViewClient():
719+
'''
720+
UiAutomator XML to AndroidViewClient
721+
'''
722+
723+
def __init__(self, device, version):
724+
self.device = device
725+
self.version = version
726+
self.root = None
727+
self.nodeStack = []
728+
self.parent = None
729+
self.viewsById = []
730+
self.idCount = 1
731+
732+
def StartElement(self, name, attributes):
733+
'''
734+
Expat start element event handler
735+
'''
736+
if name == 'hierarchy':
737+
pass
738+
elif name == 'node':
739+
# Instantiate an Element object
740+
attributes['uniqueId'] = 'id/no_id/%d' % self.idCount
741+
bounds = re.split('[\][,]', attributes['bounds'])
742+
attributes['bounds'] = ((int(bounds[1]), int(bounds[2])), (int(bounds[4]), int(bounds[5])))
743+
self.idCount += 1
744+
child = View.factory(attributes, self.device, self.version)
745+
self.viewsById.append(child)
746+
# Push element onto the stack and make it a child of parent
747+
if not self.nodeStack:
748+
self.root = child
749+
else:
750+
self.parent = self.nodeStack[-1]
751+
self.parent.add(child)
752+
self.nodeStack.append(child)
753+
754+
def EndElement(self, name):
755+
'''
756+
Expat end element event handler
757+
'''
758+
759+
if name == 'hierarchy':
760+
pass
761+
elif name == 'node':
762+
self.nodeStack.pop( )
763+
764+
def CharacterData(self, data):
765+
'''
766+
Expat character data event handler
767+
'''
768+
769+
if data.strip( ):
770+
data = data.encode( )
771+
element = self.nodeStack[-1]
772+
element.cdata += data
773+
774+
def Parse(self, uiautomatorxml):
775+
# Create an Expat parser
776+
parser = xml.parsers.expat.ParserCreate()
777+
# Set the Expat event handlers to our methods
778+
parser.StartElementHandler = self.StartElement
779+
parser.EndElementHandler = self.EndElement
780+
parser.CharacterDataHandler = self.CharacterData
781+
# Parse the XML File
782+
parserStatus = parser.Parse(uiautomatorxml, 1)
783+
return self.root
784+
702785
class ViewClient:
703786
'''
704787
ViewClient is a I{ViewServer} client.
@@ -784,7 +867,11 @@ def __init__(self, device, serialno, adb=None, autodump=True, localport=VIEW_SER
784867
TEXT_PROPERTY = TEXT_PROPERTY_API_10
785868
elif self.build[VERSION_SDK_PROPERTY] > 16: # jelly bean 4.2
786869
global USE_UI_AUTOMATOR
870+
global TEXT_PROPERTY
871+
global ID_PROPERTY
787872
USE_UI_AUTOMATOR = True
873+
TEXT_PROPERTY = 'text'
874+
ID_PROPERTY = 'uniqueId'
788875

789876
if not USE_UI_AUTOMATOR:
790877
if startviewserver:
@@ -1051,9 +1138,9 @@ def setViews(self, received):
10511138
raise ValueError("received is empty")
10521139
self.views = []
10531140
''' 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()}. '''
1141+
self.__parseTree(received.split("\n"))
10541142
if DEBUG:
10551143
print >>sys.stderr, "there are %d views in this dump" % len(self.views)
1056-
self.__parseTree(received.split("\n"))
10571144

10581145
def setViewsFromUiAutomatorDump(self, received):
10591146
'''
@@ -1067,9 +1154,9 @@ def setViewsFromUiAutomatorDump(self, received):
10671154
raise ValueError("received is empty")
10681155
self.views = []
10691156
''' 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()}. '''
1157+
self.__parseTreeFromUiAutomatorDump(received)
10701158
if DEBUG:
10711159
print >>sys.stderr, "there are %d views in this dump" % len(self.views)
1072-
self.__parseTreeFromUiAutomatorDump(self.received)
10731160

10741161

10751162
def __splitAttrs(self, strArgs, addViewToViewsById=False):
@@ -1218,7 +1305,9 @@ def __parseTree(self, receivedLines):
12181305
lastView = child
12191306

12201307
def __parseTreeFromUiAutomatorDump(self, receivedXml):
1221-
raise RuntimeError("NOT IMPLEMENTED YET!")
1308+
parser = UiAutomator2AndroidViewClient(self.device, self.build[VERSION_SDK_PROPERTY])
1309+
self.root = parser.Parse(receivedXml)
1310+
self.views = parser.viewsById
12221311

12231312
def getRoot(self):
12241313
'''
@@ -1277,14 +1366,16 @@ def dump(self, windowId=-1, sleep=1):
12771366
if not re.search('dumped', self.device.shell('uiautomator dump /mnt/sdcard/window_dump.xml')):
12781367
raise RuntimeError('ERROR: Getting UIAutomator dump')
12791368
received = self.device.shell('cat /mnt/sdcard/window_dump.xml')
1369+
if received:
1370+
received = received.encode('ascii', 'ignore')
12801371
if DEBUG:
1281-
self.received = received.encode('ascii', 'ignore')
1372+
self.received = received
12821373
if DEBUG_RECEIVED:
12831374
print >>sys.stderr, "received %d chars" % len(received)
12841375
print >>sys.stderr
1285-
print >>sys.stderr, self.received
1376+
print >>sys.stderr, received
12861377
print >>sys.stderr
1287-
self.setViewsFromUiAutomatorDump(self.received)
1378+
self.setViewsFromUiAutomatorDump(received)
12881379
else:
12891380
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
12901381
try:

0 commit comments

Comments
 (0)