QQ交流群: 815453846 Discord: https://discord.gg/PbJhnZJKDd
有一段時間沒有維護這個專案了(可能有兩年了),但最近工作需要又重新研究一下Android原生自動化,當然又研究了Appium,對比下來一看,發現uiautomator2這個專案的運作速度是真的好快,從偵測元素到點擊,都是毫秒的,程式碼也比較好理解。真是沒想到以前竟然寫出了這麼神奇的項目,這麼好的項目怎麼能讓它落灰呢,得好好整一整,一些垃圾代碼清理清理。所以專案版本從2.xx升級到了3.xx
還在用2.xx版本的用戶,可以先看一下2to3 再決定是否要升級3.xx (我個人還是非常建議升級的)
2到3畢竟是大版升級,很多的函式刪掉了。首先刪掉的就是atx-agent,其次還有一堆atx-agent相關的函數。廢棄的功能如init.
各種依賴函式庫的版本號
UiAutomator是Google提供的用來做安卓自動化測試的一個Java庫,基於Accessibility服務。功能很強,可以對第三方App進行測試,取得螢幕上任意一個APP的任意一個控制項屬性,並對其進行任意操作,但有兩個缺點:1. 測試腳本只能使用Java語言2. 測試腳本要打包成jar或apk包上傳到設備上才能運作。
我們希望測試邏輯能夠用Python編寫,能夠在電腦上運作的時候就控製手機。這裡要非常感謝Xiaocong He (@xiaocong),他將這個想法實現了出來(見xiaocong/uiautomator),原理是在手機上運行了一個http rpc服務,將uiautomator中的功能開放出來,然後再將這些http介面封裝成Python庫。 因為xiaocong/uiautomator
這個函式庫,很久沒見更新。所以我們直接fork了一個版本,為了方便做區分我們就在後面加了個2 openatx/uiautomator2,對應的Android包源碼我也fork了一份,openatx/android-uiautomator-server
除了對原有的庫的bug進行了修復,還增加了許多新的Feature。主要有以下部分:
這裡要先說明下,因為常常有很多人問openatx/uiautomator2 不支援iOS測試,需要iOS自動化測試,可以到這個函式庫openatx/facebook-wda。
PS: 這個函式庫
https://github.com/NeteaseGame/ATX目前已經不維護了,請盡快更換。
這裡有一個快速參考,適合已經入門的人QUICK REFERENCE GUIDE,歡迎多提意見。
先準備一台(不要兩台)開啟了开发者选项
的安卓手機,連接上電腦,確保執行adb devices
可以看到連接上的裝置。
運行pip3 install -U uiautomator2
安裝uiautomator2
命令列運行python
開啟python交互視窗。然後將下面的命令輸入到視窗中。
import uiautomator2 as u2
d = u2 . connect () # connect to device
print ( d . info )
這時看到類似下面的輸出,就可以正式開始用我們這個庫了。因為這個庫功能太多,後面還有很多的內容,需要慢慢去看....
{'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', '
screenOn': True, 'sdkInt': 27, 'naturalOrientation': True}
另外為了保持穩定,還需要開啟小黄车
的懸浮窗權限。參考文章py-uiautomator2透過懸浮窗讓服務長時間可用
一般情況下都會成功,不過也可能會有意外。可以加QQ群回饋問題(群號在最上面),群組裡有很多大佬可以幫你解決問題。
Thank you to all our sponsors! ?
Empty
優秀文章推薦(歡迎QQ群組裡at我回饋)
成都-测试只会一点点
Installation
Connect to a device
Command line
Global settings
App management
UI automation
Contributors
LICENSE
Install uiautomator2
pip install -U uiautomator2
測試是否安裝成功uiautomator2 --help
UI Inspector
pip install uiautodev
# 启动
uiauto.dev
瀏覽器開啟https://uiauto.dev 查看目前裝置的介面結構。
uiauto.dev
uiauto.dev 是一個獨立與uiautomator2之外的一個項目,用來查看圖層結構的。屬於舊版項目weditor的重構版本,後續也許會收費(價格肯定物超所值),來支持當前這個項目繼續維護下去。有興趣的可以加群討論(也包含提需求) QQ群536481989
use serialno to connect device eg. 123456f
(seen from adb devices
)
import uiautomator2 as u2
d = u2 . connect ( '123456f' ) # alias for u2.connect_usb('123456f')
print ( d . info )
Serial can be passed through env-var ANDROID_SERIAL
# export ANDROID_SERIAL=123456f
d = u2 . connect ()
其中的$device_ip
代表設備的ip位址
指定裝置需要傳入--serial
如python3 -m uiautomator2 --serial bff1234
, SubCommand為子指令(screenshot, current 等)
1.0.3 Added:
python3 -m uiautomator2
equals touiautomator2
screenshot: 截圖
$ uiautomator2 screenshot screenshot.jpg
current: 取得目前套件名稱和activity
$ uiautomator2 current
{
" package " : " com.android.browser " ,
" activity " : " com.uc.browser.InnerUCMobile " ,
" pid " : 28478
}
uninstall: Uninstall app
$ uiautomator2 uninstall < package-name > # 卸载一个包
$ uiautomator2 uninstall < package-name- 1> < package-name- 2> # 卸载多个包
$ uiautomator2 uninstall --all # 全部卸载
stop: Stop app
$ uiautomator2 stop com.example.app # 停止一个app
$ uiautomator2 stop --all # 停止所有的app
doctor:
$ uiautomator2 doctor
[I 2024-04-25 19:53:36,288 __main__:101 pid:15596] uiautomator2 is OK
When python quit, the UiAutomation service also quit.
列印出程式碼背後的HTTP請求訊息
> >> d . debug = True
> >> d . info
12 : 32 : 47.182 $ curl - X POST - d '{"jsonrpc": "2.0", "id": "b80d3a488580be1f3e9cb3e926175310", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:54179/jsonrpc/0'
12 : 32 : 47.225 Response >> >
{ "jsonrpc" : "2.0" , "id" : "b80d3a488580be1f3e9cb3e926175310" , "result" :{ "currentPackageName" : "com.android.mms" , "displayHeight" : 1920 , "displayRotation" : 0 , "displaySizeDpX" : 360 , "displaySizeDpY" : 640 , "displayWidth" : 1080 , "productName"
: "odin" , "screenOn" : true , "sdkInt" : 25 , "naturalOrientation" : true }}
< << END
設定元素查找等待時間(預設20s)
d . implicitly_wait ( 10.0 ) # 也可以通过d.settings['wait_timeout'] = 10.0 修改
d ( text = "Settings" ). click () # if Settings button not show in 10s, UiObjectNotFoundError will raised
print ( "wait timeout" , d . implicitly_wait ()) # get default implicit wait
This function will have influence on click
, long_click
, drag_to
, get_text
, set_text
, clear_text
, etc.
This part showcases how to perform app management
We only support installing an APK from a URL
d . app_install ( 'http://some-domain.com/some.apk' )
# 默认的这种方法是先通过atx-agent解析apk包的mainActivity,然后调用am start -n $package/$activity启动
d . app_start ( "com.example.hello_world" )
# 使用 monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 启动
# 这种方法有个副作用,它自动会将手机的旋转锁定给关掉
d . app_start ( "com.example.hello_world" , use_monkey = True ) # start with package name
# 通过指定main activity的方式启动应用,等价于调用am start -n com.example.hello_world/.MainActivity
d . app_start ( "com.example.hello_world" , ".MainActivity" )
# equivalent to `am force-stop`, thus you could lose data
d . app_stop ( "com.example.hello_world" )
# equivalent to `pm clear`
d . app_clear ( 'com.example.hello_world' )
# stop all
d . app_stop_all ()
# stop all app except for com.examples.demo
d . app_stop_all ( excludes = [ 'com.examples.demo' ])
d . app_info ( "com.examples.demo" )
# expect output
#{
# "mainActivity": "com.github.uiautomator.MainActivity",
# "label": "ATX",
# "versionName": "1.1.7",
# "versionCode": 1001007,
# "size":1760809
#}
# save app icon
img = d . app_icon ( "com.examples.demo" )
img . save ( "icon.png" )
d . app_list_running ()
# expect output
# ["com.xxxx.xxxx", "com.github.uiautomator", "xxxx"]
pid = d . app_wait ( "com.example.android" ) # 等待应用运行, return pid(int)
if not pid :
print ( "com.example.android is not running" )
else :
print ( "com.example.android pid is %d" % pid )
d . app_wait ( "com.example.android" , front = True ) # 等待应用前台运行
d . app_wait ( "com.example.android" , timeout = 20.0 ) # 最长等待时间20s(默认)
Added in version 1.2.0
push a file to the device
# push to a folder
d . push ( "foo.txt" , "/sdcard/" )
# push and rename
d . push ( "foo.txt" , "/sdcard/bar.txt" )
# push fileobj
with open ( "foo.txt" , 'rb' ) as f :
d . push ( f , "/sdcard/" )
# push and change file access mode
d . push ( "foo.sh" , "/data/local/tmp/" , mode = 0o755 )
pull a file from the device
d . pull ( "/sdcard/tmp.txt" , "tmp.txt" )
# FileNotFoundError will raise if the file is not found on the device
d . pull ( "/sdcard/some-file-not-exists.txt" , "tmp.txt" )
# grant all the permissions
d . app_auto_grant_permissions ( "io.appium.android.apis" )
# open scheme
d . open_url ( "appname://appnamehost" )
# same as
# adb shell am start -a android.intent.action.VIEW -d "appname://appnamehost"
This part showcases how to perform common device operations:
Run a short-lived shell command with a timeout protection. (Default timeout 60s)
Note: timeout support require atx-agent >=0.3.3
adb_shell
function is deprecated. Use shell
instead.
Simple usage
output , exit_code = d . shell ( "pwd" , timeout = 60 ) # timeout 60s (Default)
# output: "/n", exit_code: 0
# Similar to command: adb shell pwd
# Since `shell` function return type is `namedtuple("ShellResponse", ("output", "exit_code"))`
# so we can do some tricks
output = d . shell ( "pwd" ). output
exit_code = d . shell ( "pwd" ). exit_code
The first argument can be list. for example
output , exit_code = d . shell ([ "ls" , "-l" ])
# output: "/....", exit_code: 0
This returns a string for stdout merged with stderr. If the command is a blocking command, shell
will also block until the command is completed or the timeout kicks in. No partial output will be eived du the mandcution du. suitable for long-running commands. The shell command given runs in a similar environment of adb shell
, which has a Linux permission level of adb
or shell
(higher than an app permission).
Run a long-running shell command (Removed)
Session represent an app lifecycle. Can be used to start app, detect app crash.
Launch and close app
sess = d . session ( "com.netease.cloudmusic" ) # start 网易云音乐
sess . close () # 停止网易云音乐
sess . restart () # 冷启动网易云音乐
Use python with
to launch and close app
with d . session ( "com.netease.cloudmusic" ) as sess :
sess ( text = "Play" ). click ()
Attach to the running app
# launch app if not running, skip launch if already running
sess = d . session ( "com.netease.cloudmusic" , attach = True )
Detect app crash
# When app is still running
sess ( text = "Music" ). click () # operation goes normal
# If app crash or quit
sess ( text = "Music" ). click () # raise SessionBrokenError
# other function calls under session will raise SessionBrokenError too
# check if session is ok.
# Warning: function name may change in the future
sess . running () # True or False
Get basic information
d . info
Below is a possible output:
{'currentPackageName': 'com.android.systemui',
'displayHeight': 1560,
'displayRotation': 0,
'displaySizeDpX': 360,
'displaySizeDpY': 780,
'displayWidth': 720,
'naturalOrientation': True,
'productName': 'ELE-AL00',
'screenOn': True,
'sdkInt': 29}
Get window size
print ( d . window_size ())
# device upright output example: (1080, 1920)
# device horizontal output example: (1920, 1080)
Get current app info. For some android devices, the output could be empty (see Output example 3 )
print ( d . app_current ())
# Output example 1: {'activity': '.Client', 'package': 'com.netease.example', 'pid': 23710}
# Output example 2: {'activity': '.Client', 'package': 'com.netease.example'}
# Output example 3: {'activity': None, 'package': None}
Wait activity
d . wait_activity ( ".ApiDemos" , timeout = 10 ) # default timeout 10.0 seconds
# Output: true of false
Get device serial number
print ( d . serial )
# output example: 74aAEDR428Z9
Get WLAN ip
print ( d . wlan_ip )
# output example: 10.0.0.1 or None
Get detailed device info d.device_info
device_info
print ( d . device_info )
Below is a possible output:
{'arch': 'arm64-v8a',
'brand': 'google',
'model': 'sdk_gphone64_arm64',
'sdk': 34,
'serial': 'EMULATOR34X1X19X0',
'version': 14}
Get of set clipboard content
設定貼簿內容或取得內容
clipboard/set_clipboard
d . clipboard = 'hello-world'
# or
d . set_clipboard ( 'hello-world' , 'label' )
Get clipboard content
get clipboard requires IME(com.github.uiautomator/.AdbKeyboard) call
d.set_input_ime()
before using it.
```python
# get clipboard content
print(d.clipboard)
```
Turn on/off screen
d . screen_on () # turn on the screen
d . screen_off () # turn off the screen
Get current screen status
d . info . get ( 'screenOn' ) # require Android >= 4.4
Press hard/soft key
d . press ( "home" ) # press the home key, with key name
d . press ( "back" ) # press the back key, with key name
d . press ( 0x07 , 0x02 ) # press keycode 0x07('0') with META ALT(0x02)
These key names are currently supported:
You can find all key code definitions at Android KeyEvnet
Unlock screen
d . unlock ()
# This is equivalent to
# 1. press("power")
# 2. swipe from left-bottom to right-top
Click on the screen
d . click ( x , y )
Double click
d . double_click ( x , y )
d . double_click ( x , y , 0.1 ) # default duration between two click is 0.1s
Long click on the screen
d . long_click ( x , y )
d . long_click ( x , y , 0.5 ) # long click 0.5s (default)
Swipe
d . swipe ( sx , sy , ex , ey )
d . swipe ( sx , sy , ex , ey , 0.5 ) # swipe for 0.5s(default)
SwipeExt 擴充功能
d . swipe_ext ( "right" ) # 手指右滑,4选1 "left", "right", "up", "down"
d . swipe_ext ( "right" , scale = 0.9 ) # 默认0.9, 滑动距离为屏幕宽度的90%
d . swipe_ext ( "right" , box = ( 0 , 0 , 100 , 100 )) # 在 (0,0) -> (100, 100) 这个区域做滑动
# 实践发现上滑或下滑的时候,从中点开始滑动成功率会高一些
d . swipe_ext ( "up" , scale = 0.8 ) # 代码会vkk
# 还可以使用Direction作为参数
from uiautomator2 import Direction
d . swipe_ext ( Direction . FORWARD ) # 页面下翻, 等价于 d.swipe_ext("up"), 只是更好理解
d . swipe_ext ( Direction . BACKWARD ) # 页面上翻
d . swipe_ext ( Direction . HORIZ_FORWARD ) # 页面水平右翻
d . swipe_ext ( Direction . HORIZ_BACKWARD ) # 页面水平左翻
Drag
d . drag ( sx , sy , ex , ey )
d . drag ( sx , sy , ex , ey , 0.5 ) # swipe for 0.5s(default)
Swipe points
# swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
# time will speed 0.2s bwtween two points
d . swipe_points ([( x0 , y0 ), ( x1 , y1 ), ( x2 , y2 )], 0.2 ))
多用於九宮格解鎖,提前獲取到每個點的相對坐標(這裡支持百分比), 更詳細的使用參考這個帖子使用u2實現九宮圖案解鎖
Touch and drap (Beta)
這個接口屬於比較底層的原始接口,感覺並不完善,不過湊合能用。註:這個地方不支援百分比
d . touch . down ( 10 , 10 ) # 模拟按下
time . sleep ( .01 ) # down 和 move 之间的延迟,自己控制
d . touch . move ( 15 , 15 ) # 模拟移动
d . touch . up ( 10 , 10 ) # 模拟抬起
Note: click, swipe, drag operations support percentage position values. Example:
d.long_click(0.5, 0.5)
means long click center of screen
Retrieve/Set device orientation
The possible orientations:
natural
or n
left
or l
right
or r
upsidedown
or u
(can not be set) # retrieve orientation. the output could be "natural" or "left" or "right" or "upsidedown"
orientation = d . orientation
# WARNING: not pass testing in my TT-M1
# set orientation and freeze rotation.
# notes: setting "upsidedown" requires Android>=4.3.
d . set_orientation ( 'l' ) # or "left"
d . set_orientation ( "l" ) # or "left"
d . set_orientation ( "r" ) # or "right"
d . set_orientation ( "n" ) # or "natural"
Freeze/Un-freeze rotation
# freeze rotation
d . freeze_rotation ()
# un-freeze rotation
d . freeze_rotation ( False )
Take screenshot
# take screenshot and save to a file on the computer, require Android>=4.2.
d . screenshot ( "home.jpg" )
# get PIL.Image formatted images. Naturally, you need pillow installed first
image = d . screenshot () # default format="pillow"
image . save ( "home.jpg" ) # or home.png. Currently, only png and jpg are supported
# get opencv formatted images. Naturally, you need numpy and cv2 installed first
import cv2
image = d . screenshot ( format = 'opencv' )
cv2 . imwrite ( 'home.jpg' , image )
# get raw jpeg data
imagebin = d . screenshot ( format = 'raw' )
open ( "some.jpg" , "wb" ). write ( imagebin )
Dump UI hierarchy
# get the UI hierarchy dump content
xml = d . dump_hierarchy ()
# compressed=True: include not import nodes
# pretty: format xml
# max_depth: limit xml depth, default 50
xml = d . dump_hierarchy ( compressed = False , pretty = False , max_depth = 50 )
Open notification or quick settings
d . open_notification ()
d . open_quick_settings ()
Selector is a handy mechanism to identify a specific UI object in the current window.
# Select the object with text 'Clock' and its className is 'android.widget.TextView'
d ( text = 'Clock' , className = 'android.widget.TextView' )
Selector supports below parameters. Refer to UiSelector Java doc for detailed information.
text
, textContains
, textMatches
, textStartsWith
className
, classNameMatches
description
, descriptionContains
, descriptionMatches
, descriptionStartsWith
checkable
, checked
, clickable
, longClickable
scrollable
, enabled
, focusable
, focused
, selected
packageName
, packageNameMatches
resourceId
, resourceIdMatches
index
, instance
children
# get the children or grandchildren
d ( className = "android.widget.ListView" ). child ( text = "Bluetooth" )
siblings
# get siblings
d ( text = "Google" ). sibling ( className = "android.widget.ImageView" )
children by text or description or instance
# get the child matching the condition className="android.widget.LinearLayout"
# and also its children or grandchildren with text "Bluetooth"
d ( className = "android.widget.ListView" , resourceId = "android:id/list" )
. child_by_text ( "Bluetooth" , className = "android.widget.LinearLayout" )
# get children by allowing scroll search
d ( className = "android.widget.ListView" , resourceId = "android:id/list" )
. child_by_text (
"Bluetooth" ,
allow_scroll_search = True ,
className = "android.widget.LinearLayout"
)
child_by_description
is to find children whose grandchildren have the specified description, other parameters being similar to child_by_text
.
child_by_instance
is to find children with has a child UI element anywhere within its sub hierarchy that is at the instance specified. It is performed on visible views without scrolling .
See below links for detailed information:
getChildByDescription
, getChildByText
, getChildByInstance
getChildByDescription
, getChildByText
, getChildByInstance
Above methods support chained invoking, eg for below hierarchy
< node index = " 0 " text = " " resource-id = " android:id/list " class = " android.widget.ListView " ...>
< node index = " 0 " text = " WIRELESS & NETWORKS " resource-id = " " class = " android.widget.TextView " .../>
< node index = " 1 " text = " " resource-id = " " class = " android.widget.LinearLayout " ...>
< node index = " 1 " text = " " resource-id = " " class = " android.widget.RelativeLayout " ...>
< node index = " 0 " text = " Wi‑Fi " resource-id = " android:id/title " class = " android.widget.TextView " .../>
node >
< node index = " 2 " text = " ON " resource-id = " com.android.settings:id/switchWidget " class = " android.widget.Switch " .../>
node >
...
node >
To click the switch widget right to the TextView 'Wi‑Fi', we need to select the switch widgets first. However, according to the UI hierarchy, more than one switch widgets exist and have almost the same properties. Selecting sameby Name noting notbypertiesst the sameby. work. Alternatively, the below selecting strategy would help:
d ( className = "android.widget.ListView" , resourceId = "android:id/list" )
. child_by_text ( "Wi‑Fi" , className = "android.widget.LinearLayout" )
. child ( className = "android.widget.Switch" )
. click ()
relative positioning
Also we can use the relative positioning methods to get the view: left
, right
, top
, bottom
.
d(A).left(B)
, selects B on the left side of A.d(A).right(B)
, selects B on the right side of A.d(A).up(B)
, selects B above A.d(A).down(B)
, selects B under A.So for above cases, we can alternatively select it with:
## select "switch" on the right side of "Wi‑Fi"
d ( text = "Wi‑Fi" ). right ( className = "android.widget.Switch" ). click ()
Multiple instances
Sometimes the screen may contain multiple views with the same properties, eg text, then you will have to use the "instance" property in the selector to pick one of qualifying instances, like below:
d ( text = "Add new" , instance = 0 ) # which means the first instance with text "Add new"
In addition, uiautomator2 provides a list-like API (similar to jQuery):
# get the count of views with text "Add new" on current screen
d ( text = "Add new" ). count
# same as count property
len ( d ( text = "Add new" ))
# get the instance via index
d ( text = "Add new" )[ 0 ]
d ( text = "Add new" )[ 1 ]
...
# iterator
for view in d ( text = "Add new" ):
view . info # ...
Notes : when using selectors in a code block that walk through the result list, you must ensure that the UI elements on the screen keep unchanged. Otherwise, when Element-Not-Found error could occurcuren ilistter
Check if the specific UI object exists
d ( text = "Settings" ). exists # True if exists, else False
d . exists ( text = "Settings" ) # alias of above property.
# advanced usage
d ( text = "Settings" ). exists ( timeout = 3 ) # wait Settings appear in 3s, same as .wait(3)
Retrieve the info of the specific UI object
d ( text = "Settings" ). info
Below is a possible output:
{ u'contentDescription': u'',
u'checked': False,
u'scrollable': False,
u'text': u'Settings',
u'packageName': u'com.android.launcher',
u'selected': False,
u'enabled': True,
u'bounds': {u'top': 385,
u'right': 360,
u'bottom': 585,
u'left': 200},
u'className': u'android.widget.TextView',
u'focused': False,
u'focusable': True,
u'clickable': True,
u'chileCount': 0,
u'longClickable': True,
u'visibleBounds': {u'top': 385,
u'right': 360,
u'bottom': 585,
u'left': 200},
u'checkable': False
}
Get/Set/Clear text of an editable field (eg, EditText widgets)
d ( text = "Settings" ). get_text () # get widget text
d ( text = "Settings" ). set_text ( "My text..." ) # set the text
d ( text = "Settings" ). clear_text () # clear the text
Get Widget center point
x , y = d ( text = "Settings" ). center ()
# x, y = d(text="Settings").center(offset=(0, 0)) # left-top x, y
Take screenshot of widget
im = d ( text = "Settings" ). screenshot ()
im . save ( "settings.jpg" )
Perform click on the specific object
# click on the center of the specific ui object
d ( text = "Settings" ). click ()
# wait element to appear for at most 10 seconds and then click
d ( text = "Settings" ). click ( timeout = 10 )
# click with offset(x_offset, y_offset)
# click_x = x_offset * width + x_left_top
# click_y = y_offset * height + y_left_top
d ( text = "Settings" ). click ( offset = ( 0.5 , 0.5 )) # Default center
d ( text = "Settings" ). click ( offset = ( 0 , 0 )) # click left-top
d ( text = "Settings" ). click ( offset = ( 1 , 1 )) # click right-bottom
# click when exists in 10s, default timeout 0s
clicked = d ( text = 'Skip' ). click_exists ( timeout = 10.0 )
# click until element gone, return bool
is_gone = d ( text = "Skip" ). click_gone ( maxretry = 10 , interval = 1.0 ) # maxretry default 10, interval default 1.0
Perform long click on the specific UI object
# long click on the center of the specific UI object
d ( text = "Settings" ). long_click ()
Drag the UI object towards another point or another UI object
# notes : drag can not be used for Android<4.3.
# drag the UI object to a screen point (x, y), in 0.5 second
d ( text = "Settings" ). drag_to ( x , y , duration = 0.5 )
# drag the UI object to (the center position of) another UI object, in 0.25 second
d ( text = "Settings" ). drag_to ( text = "Clock" , duration = 0.25 )
Swipe from the center of the UI object to its edge
Swipe supports 4 directions:
d ( text = "Settings" ). swipe ( "right" )
d ( text = "Settings" ). swipe ( "left" , steps = 10 )
d ( text = "Settings" ). swipe ( "up" , steps = 20 ) # 1 steps is about 5ms, so 20 steps is about 0.1s
d ( text = "Settings" ). swipe ( "down" , steps = 20 )
Two-point gesture from one point to another
d ( text = "Settings" ). gesture (( sx1 , sy1 ), ( sx2 , sy2 ), ( ex1 , ey1 ), ( ex2 , ey2 ))
Two-point gesture on the specific UI object
Supports two gestures:
In
, from edge to centerOut
, from center to edge # notes : pinch can not be set until Android 4.3.
# from edge to center. here is "In" not "in"
d ( text = "Settings" ). pinch_in ( percent = 100 , steps = 10 )
# from center to edge
d ( text = "Settings" ). pinch_out ()
Wait until the specific UI appears or disappears
# wait until the ui object appears
d ( text = "Settings" ). wait ( timeout = 3.0 ) # return bool
# wait until the ui object gone
d ( text = "Settings" ). wait_gone ( timeout = 1.0 )
The default timeout is 20s. see global settings for more details
Perform fling on the specific ui object(scrollable)
Possible properties:
horiz
or vert
forward
or backward
or toBeginning
or toEnd
# fling forward(default) vertically(default)
d ( scrollable = True ). fling ()
# fling forward horizontally
d ( scrollable = True ). fling . horiz . forward ()
# fling backward vertically
d ( scrollable = True ). fling . vert . backward ()
# fling to beginning horizontally
d ( scrollable = True ). fling . horiz . toBeginning ( max_swipes = 1000 )
# fling to end vertically
d ( scrollable = True ). fling . toEnd ()
Perform scroll on the specific ui object(scrollable)
Possible properties:
horiz
or vert
forward
or backward
or toBeginning
or toEnd
, or to
# scroll forward(default) vertically(default)
d ( scrollable = True ). scroll ( steps = 10 )
# scroll forward horizontally
d ( scrollable = True ). scroll . horiz . forward ( steps = 100 )
# scroll backward vertically
d ( scrollable = True ). scroll . vert . backward ()
# scroll to beginning horizontally
d ( scrollable = True ). scroll . horiz . toBeginning ( steps = 100 , max_swipes = 1000 )
# scroll to end vertically
d ( scrollable = True ). scroll . toEnd ()
# scroll forward vertically until specific ui object appears
d ( scrollable = True ). scroll . to ( text = "Security" )
目前的這個watch_context是用threading啟動的,每2s檢查一次目前還只有click這一種觸發操作
with d . watch_context () as ctx :
# 当同时出现 (立即下载 或 立即更新)和 取消 按钮的时候,点击取消
ctx . when ( "^立即(下载|更新)" ). when ( "取消" ). click ()
ctx . when ( "同意" ). click ()
ctx . when ( "确定" ). click ()
# 上面三行代码是立即执行完的,不会有什么等待
ctx . wait_stable () # 开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定)
# 使用call函数来触发函数回调
# call 支持两个参数,d和el,不区分参数位置,可以不传参,如果传参变量名不能写错
# eg: 当有元素匹配仲夏之夜,点击返回按钮
ctx . when ( "仲夏之夜" ). call ( lambda d : d . press ( "back" ))
ctx . when ( "确定" ). call ( lambda el : el . click ())
# 其他操作
# 为了方便也可以使用代码中默认的弹窗监控逻辑
# 下面是目前内置的默认逻辑,可以加群at群主,增加新的逻辑,或者直接提pr
# when("继续使用").click()
# when("移入管控").when("取消").click()
# when("^立即(下载|更新)").when("取消").click()
# when("同意").click()
# when("^(好的|确定)").click()
with d . watch_context ( builtin = True ) as ctx :
# 在已有的基础上增加
ctx . when ( "@tb:id/jview_view" ). when ( '//*[@content-desc="图片"]' ). click ()
# 其他脚本逻辑
另外一種寫法
ctx = d . watch_context ()
ctx . when ( "设置" ). click ()
ctx . wait_stable () # 等待界面不在有弹窗了
ctx . close ()
更推薦用WatchContext寫法更簡潔一些
You can register watchers to perform some actions when a selector does not find a match.
2.0.0之前使用的是uiautomator-jar庫中提供的[Watcher]((http://developer.android.com/tools/help/uiautomator/UiWatcher.html)方法,但在實踐中發現一旦uiautomator連接失敗重啟了,所有的watcher配置都是遺失,這肯定是無法接受的。
所以目前採用了後台運行了一個執行緒的方法(依賴threading函式庫),然後每隔一段時間dump一次hierarchy,匹配到元素之後執行對應的操作。
用法舉例
註冊監控
# 常用写法,注册匿名监控
d . watcher . when ( "安装" ). click ()
# 注册名为ANR的监控,当出现ANR和Force Close时,点击Force Close
d . watcher ( "ANR" ). when ( xpath = "ANR" ). when ( "Force Close" ). click ()
# 其他回调例子
d . watcher . when ( "抢红包" ). press ( "back" )
d . watcher . when ( "//*[@text = 'Out of memory']" ). call ( lambda d : d . shell ( 'am force-stop com.im.qq' ))
# 回调说明
def click_callback ( d : u2 . Device ):
d . xpath ( "确定" ). click () # 在回调中调用不会再次触发watcher
d . xpath ( "继续" ). click () # 使用d.xpath检查元素的时候,会触发watcher(目前最多触发5次)
# 开始后台监控
d . watcher . start ()
監控操作
# 移除ANR的监控
d . watcher . remove ( "ANR" )
# 移除所有的监控
d . watcher . remove ()
# 开始后台监控
d . watcher . start ()
d . watcher . start ( 2.0 ) # 默认监控间隔2.0s
# 强制运行所有监控
d . watcher . run ()
# 停止监控
d . watcher . stop ()
# 停止并移除所有的监控,常用于初始化
d . watcher . reset ()
另外文件還是有很多沒寫,推薦直接去看原始碼watcher.py
u2 . HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间
其他的配置,目前已大部分集中到d.settings
中,根據後期的需求配置可能會有增減。
print ( d . settings )
{ 'operation_delay' : ( 0 , 0 ),
'operation_delay_methods' : [ 'click' , 'swipe' ],
'wait_timeout' : 20.0 }
# 配置点击前延时0.5s,点击后延时1s
d . settings [ 'operation_delay' ] = ( .5 , 1 )
# 修改延迟生效的方法
# 其中 double_click, long_click 都对应click
d . settings [ 'operation_delay_methods' ] = [ 'click' , 'swipe' , 'drag' , 'press' ]
d . settings [ 'wait_timeout' ] = 20.0 # 默认控件等待时间(原生操作,xpath插件的等待时间)
d . settings [ 'max_depth' ] = 50 # 默认50,限制dump_hierarchy返回的元素层级
對於隨著版本升級,設定過期的配置時,會提示Deprecated,但不會拋異常。
>>> d.settings[ ' click_before_delay ' ] = 1
[W 200514 14:55:59 settings:72] d.settings[click_before_delay] deprecated: Use operation_delay instead
uiautomator恢復方式設定
細心的你可能發現,其實手機安裝了兩個APK,一個在前台可見(小黃車)。一個套件名為com.github.uiautomator.test
在後台不可見。這兩個apk使用同一個憑證簽署的。 不可見的應用程式實際上是一個測試包,包含所有的測試程式碼,核心的測試服務也是透過其啟動的。 但是運行的時候,系統卻需要那個小黃車一直在運作(在後台運行也可以)。一旦小黃車應用程式被殺,後台運行的測試服務也很快的會被殺掉。就算什麼都不做,應用應用在後台,也會很快被系統回收。 (這裡希望高手指點一下,如何不依賴小黃車應用,感覺理論上是可以的,但是目前我還不會)。
讓小黃車在後台運行有兩種方式,一種啟動應用後,放到後台(預設)。另外透過am startservice
啟動一個後台服務也行。
透過d.settings["uiautomator_runtest_app_background"] = True
可以調整該行為。 True代表啟動應用,False代表啟動服務。
UiAutomator中的逾時設定(隱藏方法)
>> d . jsonrpc . getConfigurator ()
{ 'actionAcknowledgmentTimeout' : 500 ,
'keyInjectionDelay' : 0 ,
'scrollAcknowledgmentTimeout' : 200 ,
'waitForIdleTimeout' : 0 ,
'waitForSelectorTimeout' : 0 }
>> d . jsonrpc . setConfigurator ({ "waitForIdleTimeout" : 100 })
{ 'actionAcknowledgmentTimeout' : 500 ,
'keyInjectionDelay' : 0 ,
'scrollAcknowledgmentTimeout' : 200 ,
'waitForIdleTimeout' : 100 ,
'waitForSelectorTimeout' : 0 }
為了防止客戶端程式回應逾時, waitForIdleTimeout
和waitForSelectorTimeout
目前已改為0
Refs: Google uiautomator Configurator
這種方法通常用於不知道控制項的情況下的輸入。
# 目前采用从剪贴板粘贴的方式输入
d . send_keys ( "你好123abcEFG" )
d . send_keys ( "你好123abcEFG" , clear = True )
d . clear_text () # 清除输入框所有内容
d . send_action () # 根据输入框的需求,自动执行回车、搜索等指令, Added in version 3.1
# 也可以指定发送的输入法action, eg: d.send_action("search") 支持 go, search, send, next, done, previous
print ( d . current_ime ()) # 获取当前输入法ID
更多參考: IME_ACTION_CODE
print ( d . last_toast ) # get last toast, if not toast return None
d . clear_toast ()
Fixed in version 3.2.0
Java uiautoamtor中預設是不支援xpath的,所以這裡屬於擴充的一個功能。速度不是這麼的快。
For example: 其中一個節點的內容
">< android .widget.TextView index = " 2 " text = " 05:19 " resource-id = " com.netease.cloudmusic:id/qf " package = " com.netease.cloudmusic " content-desc = " " checkable = " false " checked = " false " clickable = " false " enabled = " true " focusable = " false " focused = " false " scrollable = " false " long-clickable = " false " password = " false " selected = " false " visible-to-user = " true " bounds = " [957,1602][1020,1636] " />
xpath定位與使用方法
有些屬性的名字有修改需要注意
description -> content-desc
resourceId -> resource-id
常見用法
# wait exists 10s
d . xpath ( "//android.widget.TextView" ). wait ( 10.0 )
# find and click
d . xpath ( "//*[@content-desc='分享']" ). click ()
# check exists
if d . xpath ( "//android.widget.TextView[contains(@text, 'Se')]" ). exists :
print ( "exists" )
# get all text-view text, attrib and center point
for elem in d . xpath ( "//android.widget.TextView" ). all ():
print ( "Text:" , elem . text )
# Dictionary eg:
# {'index': '1', 'text': '999+', 'resource-id': 'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false', 'enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false', 'long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}
print ( "Attrib:" , elem . attrib )
# Coordinate eg: (100, 200)
print ( "Position:" , elem . center ())
點擊查看其他XPath常見用法
錄影(廢棄),使用scrcpy來代替吧
這裡沒有使用手機中自帶的screenrecord指令,是透過取得手機圖片合成影片的方法,所以需要安裝一些其他的依賴,如imageio, imageio-ffmpeg, numpy等因為有些依賴比較大,推薦使用鏡像安裝。直接運行下面的命令即可。
pip3 install -U " uiautomator2[image] " -i https://pypi.doubanio.com/simple
使用方法
d.screenrecord('output.mp4')
time.sleep(10)
# or do something else
d.screenrecord.stop() # 停止录制后,output.mp4文件才能打开
錄影的時候也可以指定fps(目前是20),這個值是率低於minicap輸出圖片的速度,感覺已經很好了,不建議你修改。
from uiautomator2 import enable_pretty_logging
enable_pretty_logging ()
Or
logger = logging.getLogger("uiautomator2")
# setup logger
Python程式退出了,UiAutomation就退出了。 不過也可以透過介面的方法停止服務
d . stop_uiautomator ()
https://www.cnblogs.com/insist8089/p/6898181.html
Other contributors
排名先後,歡迎補充
MIT