Skip to content

Commit a089b64

Browse files
committed
Module variables converted to class members to support multiple instances with different properties
- Corrected dump.py script typo - Added WINDOW_DUMP (uiautomator dump) mock - Added properties tests
1 parent 887e016 commit a089b64

File tree

4 files changed

+145
-30
lines changed

4 files changed

+145
-30
lines changed

AndroidViewClient/examples/dump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
CONTENT_DESCRIPTION = 'content-description'
3434
MAP = {'u':ViewClient.TRAVERSE_CITUI, UNIQUE_ID:ViewClient.TRAVERSE_CITUI,
3535
'x':ViewClient.TRAVERSE_CITPS, POSITION:ViewClient.TRAVERSE_CITPS,
36-
'd':ViewClient.TRAVERSE_CITPS, CONTENT_DESCRIPTION:ViewClient.TRAVERSE_CITCD,
36+
'd':ViewClient.TRAVERSE_CITCD, CONTENT_DESCRIPTION:ViewClient.TRAVERSE_CITCD,
3737
}
3838

3939
def usage():

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

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
and touches it at M{(x+OFFSET, y+OFFSET)} '''
5555

5656
USE_MONKEYRUNNER_TO_GET_BUILD_PROPERTIES = True
57-
58-
USE_UI_AUTOMATOR = False
57+
''' Use monkeyrunner (C{MonkeyDevice.getProperty()}) to obtain the needed properties. If this is
58+
C{False} then C{adb shell getprop} is used '''
5959

6060
SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED = False
6161
''' Skips some classes related with the Action Bar and the PhoneWindow$DecorView in the
@@ -67,8 +67,10 @@
6767

6868
# some constants for the attributes
6969
ID_PROPERTY = 'mID'
70+
ID_PROPERTY_UI_AUTOMATOR = 'uniqueId'
7071
TEXT_PROPERTY = 'text:mText'
7172
TEXT_PROPERTY_API_10 = 'mText'
73+
TEXT_PROPERTY_UI_AUTOMATOR = 'text'
7274
WS = "\xfe" # the whitespace replacement char for TEXT_PROPERTY
7375
GET_VISIBILITY_PROPERTY = 'getVisibility()'
7476
LAYOUT_TOP_MARGIN_PROPERTY = 'layout:layout_topMargin'
@@ -214,7 +216,29 @@ def __init__(self, map, device, version=-1):
214216
self.build[VERSION_SDK_PROPERTY] = int(device.shell('getprop ro.build.' + VERSION_SDK_PROPERTY)[:-2])
215217
except:
216218
self.build[VERSION_SDK_PROPERTY] = -1
217-
219+
220+
version = self.build[VERSION_SDK_PROPERTY]
221+
self.useUiAutomator = (version >= 16)
222+
''' Whether to use UIAutomator or ViewServer '''
223+
self.idProperty = None
224+
''' The id property depending on the View attribute format '''
225+
self.textProperty = None
226+
''' The text property depending on the View attribute format '''
227+
if version >= 16:
228+
self.idProperty = ID_PROPERTY_UI_AUTOMATOR
229+
self.textProperty = TEXT_PROPERTY_UI_AUTOMATOR
230+
elif version > 10 and version < 16:
231+
self.idProperty = ID_PROPERTY
232+
self.textProperty = TEXT_PROPERTY
233+
elif version > 0 and version <= 10:
234+
self.idProperty = ID_PROPERTY
235+
self.textProperty = TEXT_PROPERTY_API_10
236+
elif version == -1:
237+
self.idProperty = ID_PROPERTY
238+
self.textProperty = TEXT_PROPERTY
239+
else:
240+
self.idProperty = ID_PROPERTY
241+
self.textProperty = TEXT_PROPERTY
218242

219243
def __getitem__(self, key):
220244
return self.map[key]
@@ -287,7 +311,7 @@ def getId(self):
287311
'''
288312

289313
try:
290-
return self.map[ID_PROPERTY]
314+
return self.map[self.idProperty]
291315
except:
292316
return None
293317

@@ -312,12 +336,12 @@ def getText(self):
312336
'''
313337

314338
try:
315-
return self.map[TEXT_PROPERTY]
339+
return self.map[self.textProperty]
316340
except Exception:
317341
return None
318342

319343
def getHeight(self):
320-
if USE_UI_AUTOMATOR:
344+
if self.useUiAutomator:
321345
return self.map['bounds'][1][1] - self.map['bounds'][0][1]
322346
else:
323347
try:
@@ -326,7 +350,7 @@ def getHeight(self):
326350
return 0
327351

328352
def getWidth(self):
329-
if USE_UI_AUTOMATOR:
353+
if self.useUiAutomator:
330354
return self.map['bounds'][1][0] - self.map['bounds'][0][0]
331355
else:
332356
try:
@@ -372,7 +396,7 @@ def getX(self):
372396
print >>sys.stderr, "getX(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
373397
x = 0
374398

375-
if USE_UI_AUTOMATOR:
399+
if self.useUiAutomator:
376400
x = self.map['bounds'][0][0]
377401
else:
378402
try:
@@ -394,7 +418,7 @@ def getY(self):
394418
print >>sys.stderr, "getY(%s %s ## %s)" % (self.getClass(), self.getId(), self.getUniqueId())
395419
y = 0
396420

397-
if USE_UI_AUTOMATOR:
421+
if self.useUiAutomator:
398422
y = self.map['bounds'][0][1]
399423
else:
400424
try:
@@ -424,7 +448,7 @@ def getXY(self):
424448
hx = 0
425449
hy = 0
426450

427-
if not USE_UI_AUTOMATOR:
451+
if not self.useUiAutomator:
428452
while parent != None:
429453
if DEBUG_COORDS: print >> sys.stderr, " getXY: parent: %s %s <<<<" % (parent.getClass(), parent.getId())
430454
if SKIP_CERTAIN_CLASSES_IN_GET_XY_ENABLED:
@@ -879,18 +903,10 @@ def __init__(self, device, serialno, adb=None, autodump=True, localport=VIEW_SER
879903
# we expect it to be an int
880904
self.build[prop] = int(self.build[prop] if self.build[prop] else -1)
881905

882-
if self.build[VERSION_SDK_PROPERTY] > 0 and self.build[VERSION_SDK_PROPERTY] <= 10: # gingerbread 2.3.3
883-
global TEXT_PROPERTY
884-
TEXT_PROPERTY = TEXT_PROPERTY_API_10
885-
elif self.build[VERSION_SDK_PROPERTY] >= 16: # jelly bean 4.1 & 4.2
886-
global USE_UI_AUTOMATOR
887-
global TEXT_PROPERTY
888-
global ID_PROPERTY
889-
USE_UI_AUTOMATOR = True
890-
TEXT_PROPERTY = 'text'
891-
ID_PROPERTY = 'uniqueId'
892-
893-
if not USE_UI_AUTOMATOR:
906+
self.useUiAutomator = (self.build[VERSION_SDK_PROPERTY] >= 16) # jelly bean 4.1 & 4.2
907+
''' If UIAutomator is supported by the device it will be used '''
908+
909+
if not self.useUiAutomator:
894910
if startviewserver:
895911
if not self.serviceResponse(device.shell('service call window 3')):
896912
try:
@@ -1217,6 +1233,8 @@ def __splitAttrs(self, strArgs, addViewToViewsById=False):
12171233
@return: Returns the attributes map.
12181234
'''
12191235

1236+
if self.useUiAutomator:
1237+
raise RuntimeError("This method is not compatible with UIAutomator")
12201238
# replace the spaces in text:mText to preserve them in later split
12211239
# they are translated back after the attribute matches
12221240
textRE = re.compile('%s=%s,' % (TEXT_PROPERTY, __nd('len')))
@@ -1398,12 +1416,15 @@ def dump(self, windowId=-1, sleep=1):
13981416
if sleep > 0:
13991417
MonkeyRunner.sleep(sleep)
14001418

1401-
if USE_UI_AUTOMATOR:
1402-
if not re.search('dumped', self.device.shell('uiautomator dump /mnt/sdcard/window_dump.xml')):
1419+
if self.useUiAutomator:
1420+
windowDump = '/mnt/sdcard/window_dump.xml'
1421+
if not re.search('dumped', self.device.shell('uiautomator dump %s' % windowDump)):
14031422
raise RuntimeError('ERROR: Getting UIAutomator dump')
1404-
received = self.device.shell('cat /mnt/sdcard/window_dump.xml')
1423+
received = self.device.shell('cat %s 2>/dev/null' % windowDump)
14051424
if received:
14061425
received = received.encode('ascii', 'ignore')
1426+
else:
1427+
raise RuntimeError("ERROR: Received empty UIAutomator dump")
14071428
if DEBUG:
14081429
self.received = received
14091430
if DEBUG_RECEIVED:

AndroidViewClient/tests/com/dtmilano/android/mocks.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,65 @@
310310
mStartingIconInTransition=false, mSkipAppTransitionAnimation=false
311311
"""
312312

313+
WINDOW_DUMP = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
314+
<hierarchy rotation="0">
315+
<node index="0" text="" class="android.widget.FrameLayout"
316+
package="com.android.launcher" content-desc="" checkable="false"
317+
checked="false" clickable="false" enabled="true" focusable="false"
318+
focused="false" scrollable="false" long-clickable="false" password="false"
319+
selected="false" bounds="[0,0][480,800]">
320+
<node index="0" text="" class="android.widget.LinearLayout"
321+
package="com.android.launcher" content-desc="" checkable="false"
322+
checked="false" clickable="false" enabled="true" focusable="false"
323+
focused="false" scrollable="false" long-clickable="false" password="false"
324+
selected="false" bounds="[0,0][480,800]">
325+
<node index="0" text="" class="android.widget.FrameLayout"
326+
package="com.android.launcher" content-desc="" checkable="false"
327+
checked="false" clickable="false" enabled="true" focusable="false"
328+
focused="false" scrollable="false" long-clickable="false" password="false"
329+
selected="false" bounds="[0,38][480,800]">
330+
<node index="0" text="" class="android.widget.FrameLayout"
331+
package="com.android.launcher" content-desc="" checkable="false"
332+
checked="false" clickable="false" enabled="true" focusable="false"
333+
focused="false" scrollable="false" long-clickable="false" password="false"
334+
selected="false" bounds="[0,38][480,800]">
335+
<node index="0" text="" class="android.widget.TabHost"
336+
package="com.android.launcher" content-desc="" checkable="false"
337+
checked="false" clickable="false" enabled="true" focusable="true"
338+
focused="true" scrollable="false" long-clickable="false" password="false"
339+
selected="false" bounds="[0,38][480,800]">
340+
<node index="0" text="" class="android.widget.LinearLayout"
341+
package="com.android.launcher" content-desc="" checkable="false"
342+
checked="false" clickable="false" enabled="true" focusable="false"
343+
focused="false" scrollable="false" long-clickable="false"
344+
password="false" selected="false" bounds="[0,38][480,800]">
345+
<node index="0" text="" class="android.widget.FrameLayout"
346+
package="com.android.launcher" content-desc="" checkable="false"
347+
checked="false" clickable="false" enabled="true" focusable="false"
348+
focused="false" scrollable="false" long-clickable="false"
349+
password="false" selected="false" bounds="[1,38][479,116]">
350+
<node index="0" text="" class="android.widget.TabWidget"
351+
package="com.android.launcher" content-desc="" checkable="false"
352+
checked="false" clickable="false" enabled="true" focusable="true"
353+
focused="false" scrollable="false" long-clickable="false"
354+
password="false" selected="false" bounds="[1,38][479,116]">
355+
<node index="0" text="Apps" class="android.widget.TextView"
356+
package="com.android.launcher" content-desc="Apps" checkable="false"
357+
checked="false" clickable="true" enabled="true" focusable="true"
358+
focused="false" scrollable="false" long-clickable="false"
359+
password="false" selected="true" bounds="[1,38][105,116]" />
360+
</node>
361+
</node>
362+
</node>
363+
</node>
364+
</node>
365+
</node>
366+
</node>
367+
</node>
368+
</hierarchy>
369+
"""
370+
371+
313372
RUNNING = 1
314373
STOPPED = 0
315374

@@ -341,6 +400,13 @@ def shell(self, cmd):
341400
elif cmd == 'dumpsys window windows':
342401
return DUMPSYS_WINDOW_WINDOWS
343402

403+
m = re.match('uiautomator dump (\S+)', cmd)
404+
if m and self.version >= 16:
405+
return 'dumped %s' % m.group(1)
406+
m = re.match('cat (\S+) .*', cmd)
407+
if m:
408+
return WINDOW_DUMP
409+
344410
def getProperty(self, property):
345411
if property == 'ro.serialno':
346412
return self.serialno

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,18 @@ def testViewFactory_TextView(self):
6060
self.assertTrue(isinstance(view, EditText))
6161

6262
def testView_notSpecifiedSdkVersion(self):
63-
view = View(VIEW_MAP, MockDevice(), -1)
64-
self.assertEqual(15, view.build[VERSION_SDK_PROPERTY])
63+
device = MockDevice()
64+
view = View(VIEW_MAP, device, -1)
65+
self.assertEqual(device.version, view.build[VERSION_SDK_PROPERTY])
6566

66-
def testView_specifiedSdkVersion(self):
67+
def testView_specifiedSdkVersion_10(self):
6768
view = View(VIEW_MAP, MockDevice(), 10)
6869
self.assertEqual(10, view.build[VERSION_SDK_PROPERTY])
6970

71+
def testView_specifiedSdkVersion_16(self):
72+
view = View(VIEW_MAP, MockDevice(), 16)
73+
self.assertEqual(16, view.build[VERSION_SDK_PROPERTY])
74+
7075
def testInnerMethod(self):
7176
v = View({'isChecked()':'true'}, None)
7277
self.assertTrue(v.isChecked())
@@ -89,6 +94,25 @@ def testGetClass(self):
8994
def testGetId(self):
9095
self.assertEqual('id/button_with_id', self.view.getId())
9196

97+
def testTextPropertyForDifferentSdkVersions(self):
98+
TEXT_PROPERTY = 'text:mText'
99+
TEXT_PROPERTY_API_10 = 'mText'
100+
TEXT_PROPERTY_UI_AUTOMATOR = 'text'
101+
VTP = { -1:TEXT_PROPERTY, 8:TEXT_PROPERTY_API_10, 10:TEXT_PROPERTY_API_10, 15:TEXT_PROPERTY, 16:TEXT_PROPERTY_UI_AUTOMATOR, 17:TEXT_PROPERTY_UI_AUTOMATOR}
102+
for version, textProperty in VTP.items():
103+
view = View(None, None, version)
104+
self.assertEqual(textProperty, view.textProperty, msg='version %d' % version)
105+
106+
def testTextPropertyForDifferentSdkVersions_device(self):
107+
TEXT_PROPERTY = 'text:mText'
108+
TEXT_PROPERTY_API_10 = 'mText'
109+
TEXT_PROPERTY_UI_AUTOMATOR = 'text'
110+
VTP = { -1:TEXT_PROPERTY, 8:TEXT_PROPERTY_API_10, 10:TEXT_PROPERTY_API_10, 15:TEXT_PROPERTY, 16:TEXT_PROPERTY_UI_AUTOMATOR, 17:TEXT_PROPERTY_UI_AUTOMATOR}
111+
for version, textProperty in VTP.items():
112+
device = MockDevice(version=version)
113+
view = View(None, device, -1)
114+
self.assertEqual(textProperty, view.textProperty, msg='version %d' % version)
115+
92116
def testGetText(self):
93117
self.assertTrue(self.view.map.has_key('text:mText'))
94118
self.assertEqual('Button with ID', self.view.getText())
@@ -183,7 +207,7 @@ def testConnectToDeviceOrExit_environ(self):
183207
sys.argv = ['']
184208
os.environ['ANDROID_SERIAL'] = 'ABC123'
185209
try:
186-
ViewClient.connectToDeviceOrExit(verbose=True)
210+
ViewClient.connectToDeviceOrExit(timeout=1, verbose=True)
187211
except exceptions.SystemExit, e:
188212
self.assertEquals(3, e.code)
189213

@@ -498,6 +522,10 @@ def testFindViewWithTextOrRaise_rootNonExistent(self):
498522
self.fail()
499523
except ViewNotFoundException:
500524
pass
525+
526+
def testUiAutomatorDump(self):
527+
device = MockDevice(version=16)
528+
vc = ViewClient(device, device.serialno, adb=TRUE, autodump=True)
501529

502530

503531
if __name__ == "__main__":

0 commit comments

Comments
 (0)