QQ communication group: 815453846 Discord: https://discord.gg/PbJhnZJKDd
I haven't maintained this project for a while (maybe two years), but recently I need to re-research Android native automation. Of course, I also investigated Appium. After comparing it, I found that the running speed of the uiautomator2 project is really good. It’s fast, from detecting elements to clicking, it’s all in milliseconds, and the code is easier to understand. I really didn't expect that I had written such a magical project before. How could such a good project let it gather dust? It needs to be reorganized and some junk code should be cleaned up. So the project version has been upgraded from 2.xx to 3.xx
Users who are still using version 2.xx can take a look at 2to3 before deciding whether to upgrade to 3.xx (I personally highly recommend upgrading)
After all, version 2 to 3 is a major version upgrade, and many functions have been deleted. The first thing to delete is atx-agent, and secondly there are a bunch of atx-agent related functions. Deprecated functions such as init.
Version numbers of various dependent libraries
UiAutomator is a Java library provided by Google for Android automation testing, based on the Accessibility service. It is very powerful and can test third-party apps, obtain any control attribute of any APP on the screen, and perform any operation on it, but it has two disadvantages: 1. The test script can only use Java language 2. The test script It must be packaged into a jar or apk package and uploaded to the device to run.
We hope that the test logic can be written in Python and can control the mobile phone while running on the computer. I would like to thank Xiaocong He (@xiaocong) very much for realizing this idea (see xiaocong/uiautomator). The principle is to run an http rpc service on the mobile phone, open up the functions in uiautomator, and then use these http The interface is encapsulated into a Python library. Because the xiaocong/uiautomator
library has not been updated for a long time. So we directly forked a version. In order to make it easier to distinguish, we added 2 openatx/uiautomator2 at the end. I also forked a copy of the corresponding Android package source code, openatx/android-uiautomator-server.
In addition to fixing bugs in the original library, many new features have also been added. It mainly includes the following parts:
Let me explain here first, because many people often ask that openatx/uiautomator2 does not support iOS testing. If you need iOS automated testing, you can go to this library openatx/facebook-wda.
PS: This library
https://github.com/NeteaseGame/ATXIt is no longer under maintenance, please replace it as soon as possible.
Here is a quick reference QUICK REFERENCE GUIDE suitable for those who have already started. Comments are welcome.
First prepare one (not two) Android phone with开发者选项
turned on, connect it to the computer, and make sure you can see the connected device by executing adb devices
.
Run pip3 install -U uiautomator2
to install uiautomator2
Run python
from the command line to open the python interactive window. Then enter the following command into the window.
import uiautomator2 as u2
d = u2 . connect () # connect to device
print ( d . info )
When you see output similar to the one below, you can officially start using our library. Because this library has too many functions and there is a lot of content behind it, you need to read it slowly....
{'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', '
screenOn': True, 'sdkInt': 27, 'naturalOrientation': True}
In addition, in order to maintain stability, you need to enable the floating window permission of小黄车
. Reference article py-uiautomator2 makes services available for a long time through floating windows
It usually succeeds, but there may be surprises. You can join the QQ group to report problems (the group number is at the top). There are many big guys in the group who can help you solve problems.
Thank you to all our sponsors! ?
Empty
Excellent article recommendations (welcome at me in the QQ group for feedback)
成都-测试只会一点点
Installation
Connect to a device
Command line
Global settings
App management
UI automation
Contributors
LICENSE
Install uiautomator2
pip install -U uiautomator2
Test whether the installation is successful uiautomator2 --help
UI Inspector
pip install uiautodev
# 启动
uiauto.dev
Open https://uiauto.dev in the browser to view the interface structure of the current device.
uiauto.dev
uiauto.dev is a project independent of uiautomator2, used to view the layer structure. It is a reconstructed version of the old project editor, and may be charged later (the price is definitely worth the money) to support the continued maintenance of the current project. If you are interested, you can join the group for discussion (including making requests) QQ group 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
represents the IP address of the device
If you need to specify the device, you need to pass in --serial
such as python3 -m uiautomator2 --serial bff1234
, SubCommand is a subcommand (screenshot, current, etc.)
1.0.3 Added:
python3 -m uiautomator2
equals touiautomator2
screenshot: screenshot
$ uiautomator2 screenshot screenshot.jpg
current: Get the current package name and 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.
Print out the HTTP request information behind the code
> >> 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
Set element search waiting time (default 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 received during the execution of the command. This API is not 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 represents 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
Set pasteboard content or get 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 extension function
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 ))
It is mostly used to unlock the Jiugong pattern. The relative coordinates of each point can be obtained in advance (percentage is supported here). For more detailed usage, please refer to this post. Use u2 to unlock the Jiugong pattern.
Touch and drap (Beta)
This interface is a relatively low-level primitive interface. It doesn't feel perfect, but it is usable. Note: This place does not support percentages
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 by className will not 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 the 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 occur when iterating through the list.
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" )
The current watch_context is started using threading and is checked every 2 seconds. Currently, there is only one trigger operation: 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 ()
# 其他脚本逻辑
Another way to write
ctx = d . watch_context ()
ctx . when ( "设置" ). click ()
ctx . wait_stable () # 等待界面不在有弹窗了
ctx . close ()
It is more recommended to use WatchContext to write it more concisely.
You can register watchers to perform some actions when a selector does not find a match.
Before 2.0.0, the [Watcher]((http://developer.android.com/tools/help/uiautomator/UiWatcher.html) method provided in the uiautomator-jar library was used, but in practice it was found that once the uiautomator connection failed After restarting, all watcher configurations are lost, which is definitely unacceptable.
Therefore, the current method is to run a thread in the background (relying on the threading library), and then dump the hierarchy every once in a while, and perform the corresponding operations after matching the elements.
Usage examples
Registration monitoring
# 常用写法,注册匿名监控
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 ()
Monitor operations
# 移除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 ()
In addition, there are still many documents that have not been written. It is recommended to go directly to the source code watcher.py.
u2 . HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间
Most of the other configurations are currently concentrated in d.settings
, and the configuration may be increased or decreased according to later needs.
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返回的元素层级
For version upgrades, when setting expired configurations, Deprecated will be prompted, but no exception will be thrown.
>>> d.settings[ ' click_before_delay ' ] = 1
[W 200514 14:55:59 settings:72] d.settings[click_before_delay] deprecated: Use operation_delay instead
uiautomator recovery mode settings
If you are careful, you may find that there are actually two APKs installed on your phone, one of which is visible in the foreground (the little yellow car). A package named com.github.uiautomator.test
is not visible in the background. These two apks are signed with the same certificate. The invisible application is actually a test package that contains all the test code, and the core test service is also started through it. But when running, the system needs the little yellow car to be running all the time (it can also be run in the background). Once the little yellow car application is killed, the test service running in the background will also be killed quickly. Even if you do nothing, the application will be quickly recycled by the system if it is in the background. (I hope experts can give me some guidance here on how to avoid relying on fake apps. I think it’s theoretically possible, but I don’t know how to do it yet).
There are two ways to let the little yellow car run in the background. One is to start the application and put it in the background (default). In addition, you can also start a background service through am startservice
.
This behavior can be adjusted via d.settings["uiautomator_runtest_app_background"] = True
. True means starting the application, False means starting the service.
Timeout settings in UiAutomator (hidden method)
>> 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 }
In order to prevent the client program from responding to a timeout, waitForIdleTimeout
and waitForSelectorTimeout
have now been changed to 0
Refs: Google uiautomator Configurator
This method is typically used for input where the control is unknown.
# 目前采用从剪贴板粘贴的方式输入
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
More reference: 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 does not support xpath by default, so this is an extended function. The speed is not that fast.
For example: the contents of one of the nodes
">< 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 positioning and usage
The names of some attributes have been modified and need to be noted.
description -> content-desc
resourceId -> resource-id
Common usage
# 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 ())
Click to see other common uses of XPath
Video recording (obsolete), use scrcpy instead
The screenrecord command that comes with the mobile phone is not used here. It is a method of synthesizing video by obtaining mobile phone pictures, so you need to install some other dependencies, such as imageio, imageio-ffmpeg, numpy, etc. Because some dependencies are relatively large, it is recommended to use mirror installation. Just run the following command.
pip3 install -U " uiautomator2[image] " -i https://pypi.doubanio.com/simple
How to use
d.screenrecord('output.mp4')
time.sleep(10)
# or do something else
d.screenrecord.stop() # 停止录制后,output.mp4文件才能打开
You can also specify fps (currently 20) when recording. This value is lower than the speed of minicap output pictures. It feels very good. I don’t recommend you to modify it.
from uiautomator2 import enable_pretty_logging
enable_pretty_logging ()
Or
logger = logging.getLogger("uiautomator2")
# setup logger
When the Python program exits, UiAutomation exits. However, you can also stop the service through the interface method.
d . stop_uiautomator ()
https://www.cnblogs.com/insist8089/p/6898181.html
Other contributors
Ranking is in order, welcome to add
MIT