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>
, 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