uiautomator2

📖 Read the Chinese version
A simple, easy-to-use, and stable Android automation library.
Users still on version 2.x.x, please check 2to3 before deciding to upgrade to 3.x.x (Upgrade is highly recommended).
How it Works
This framework mainly consists of two parts:
- Device Side: Runs an HTTP service based on UiAutomator, providing various interfaces for Android automation.
- Python Client: Communicates with the device side via HTTP protocol, invoking UiAutomator's various functions.
Simply put, it exposes Android automation capabilities to Python through HTTP interfaces. This design makes Python-side code writing simpler and more intuitive.
Dependencies
- Android version 4.4+
- Python 3.8+
Installation
pip install uiautomator2
uiautomator2 version
Install element inspection tool (optional, but highly recommended):
For more detailed usage instructions, refer to: https://github.com/codeskyblue/uiautodev QQ:536481989
pip install uiautodev
uiautodev
Alternatives: uiautomatorviewer, Appium Inspector
Quick Start
Prepare an Android phone with Developer options
enabled, connect it to the computer, and ensure that adb devices
shows the connected device.
Open a Python interactive window. Then, input the following commands into the window.
import uiautomator2 as u2
d = u2.connect()
print(d.info)
Example script:
import uiautomator2 as u2
d = u2.connect('Q5S5T19611004599')
d.app_start('tv.danmaku.bili', stop=True)
d.wait_activity('.MainActivityV2')
d.sleep(5)
d.xpath('//*[@text="我的"]').click()
fans_count = d.xpath('//*[@resource-id="tv.danmaku.bili:id/fans_count"]').text
print(f"Fan count: {fans_count}")
Documentation
Connecting to Device
Method 1: Connect using device serial number, e.g., Q5S5T19611004599
(seen from adb devices
)
import uiautomator2 as u2
d = u2.connect('Q5S5T19611004599')
print(d.info)
Method 2: Serial number can be passed via environment variable ANDROID_SERIAL
d = u2.connect()
Method 3: Specify device via transport_id
$ adb devices -l
Q5S5T19611004599 device 0-1.2.2 product:ELE-AL00 model:ELE_AL00 device:HWELE transport_id:6
Here you can see transport_id:6
.
You can also get all connected transport_ids via adbutils.adb.list(extended=True)
Refer to https://github.com/openatx/adbutils
import adbutils
import uiautomator2 as u2
dev = adbutils.device(transport_id=6)
d = u2.connect(dev)
Operating Elements with XPath
What is XPath:
XPath is a query language for locating content in XML or HTML documents. It uses simple syntax rules to establish a path from the root node to the desired element.
Basic Syntax:
/
- Select from the root node
//
- Select from any position starting from the current node
.
- Select the current node
..
- Select the parent of the current node
@
- Select attributes
[]
- Predicate expression, used for filtering conditions
You can quickly generate XPath using UIAutoDev.
Common Usage:
d.xpath('//*[@text="私人FM"]').click()
d.xpath('@personal-fm')
sl = d.xpath("@com.example:id/home_searchedit")
sl.click()
sl.click(timeout=10)
sl.click_exists()
sl.click_exists(timeout=10)
el = sl.wait()
el = sl.wait(timeout=15)
sl.wait_gone()
sl.wait_gone(timeout=15)
el = sl.get()
el = sl.get(timeout=15)
sl.get_text()
sl.set_text("")
sl.set_text("hello world")
For more usage, refer to XPath Interface Document
Operating Elements with UiAutomator API
Element Wait Timeout
Set element search wait time (default 20s)
d.implicitly_wait(10.0)
print("wait timeout", d.implicitly_wait())
d(text="Settings").click()
Wait timeout affects the following functions: click
, long_click
, drag_to
, get_text
, set_text
, clear_text
.
Get Device Information
Information obtained via UiAutomator:
d.info
{'currentPackageName': 'com.android.systemui',
'displayHeight': 1560,
'displayRotation': 0,
'displaySizeDpX': 360,
'displaySizeDpY': 780,
'displayWidth': 720,
'naturalOrientation': True,
'productName': 'ELE-AL00',
'screenOn': True,
'sdkInt': 29}
Get device information (based on adb shell getprop
command):
print(d.device_info)
{'arch': 'arm64-v8a',
'brand': 'google',
'model': 'sdk_gphone64_arm64',
'sdk': 34,
'serial': 'EMULATOR34X1X19X0',
'version': 14}
Get screen physical size (depends on adb shell wm size
):
print(d.window_size())
Get current App (depends on adb shell
):
print(d.app_current())
Wait for Activity (depends on adb shell
):
d.wait_activity(".ApiDemos", timeout=10)
Get device serial number:
print(d.serial)
Get device WLAN IP (depends on adb shell
):
print(d.wlan_ip)
Clipboard
Set or get clipboard content.
Key Events
-
Turn on/off screen
d.screen_on()
d.screen_off()
-
Get current screen status
d.info.get('screenOn')
-
Press hard/soft key
d.press("home")
d.press("back")
d.press(0x07, 0x02)
-
These key names are currently supported:
- home
- back
- left
- right
- up
- down
- center
- menu
- search
- enter
- delete ( or del)
- recent (recent apps)
- volume_up
- volume_down
- volume_mute
- camera
- power
You can find all key code definitions at Android KeyEvent
Gesture interaction with the device
-
Click on the screen
d.click(x, y)
-
Double click
d.double_click(x, y)
d.double_click(x, y, 0.1)
-
Long click on the screen
d.long_click(x, y)
d.long_click(x, y, 0.5)
-
Swipe
d.swipe(sx, sy, ex, ey)
d.swipe(sx, sy, ex, ey, 0.5)
-
SwipeExt (Extended functionality)
d.swipe_ext("right")
d.swipe_ext("right", scale=0.9)
d.swipe_ext("right", box=(0, 0, 100, 100))
d.swipe_ext("up", scale=0.8)
from uiautomator2 import Direction
d.swipe_ext(Direction.FORWARD)
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 points
d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2)
Often used for pattern unlock, get relative coordinates of each point beforehand (supports percentages).
For more detailed usage, refer to this post Using u2 to implement pattern unlock
-
Touch and drag (Beta)
This is a lower-level raw interface, feels incomplete but usable. Note: percentages are not supported here.
d.touch.down(10, 10)
time.sleep(.01)
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.
Screen Related APIs
-
Retrieve/Set device orientation
The possible orientations:
natural
or n
left
or l
right
or r
upsidedown
or u
(cannot be set)
orientation = d.orientation
d.set_orientation('l')
d.set_orientation("r")
d.set_orientation("n")
-
Freeze/Un-freeze rotation
d.freeze_rotation()
d.freeze_rotation(False)
-
Take screenshot
d.screenshot("home.jpg")
image = d.screenshot()
image.save("home.jpg")
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
imagebin = d.screenshot(format='raw')
with open("some.jpg", "wb") as f:
f.write(imagebin)
-
Dump UI hierarchy
xml = d.dump_hierarchy()
xml = d.dump_hierarchy(compressed=False, pretty=True, max_depth=30)
-
Open notification or quick settings
d.open_notification()
d.open_quick_settings()
Selector
Selector is a handy mechanism to identify a specific UI object in the current window.
d(text='Clock', className='android.widget.TextView')
Selector supports the 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 and siblings
-
children
d(className="android.widget.ListView").child(text="Bluetooth")
-
siblings
d(text="Google").sibling(className="android.widget.ImageView")
-
children by text or description or instance
d(className="android.widget.ListView", resourceId="android:id/list") \
.child_by_text("Bluetooth", className="android.widget.LinearLayout")
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 which have 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:
- UiScrollable,
getChildByDescription
, getChildByText
, getChildByInstance
- UiCollection,
getChildByDescription
, getChildByText
, getChildByInstance
Above methods support chained invoking, e.g. for the 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 widget first. However, according to the UI hierarchy, more than one switch widget exists and has 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
, up
, down
.
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:
d(text="Wi‑Fi").right(className="android.widget.Switch").click()
-
Multiple instances
Sometimes the screen may contain multiple views with the same properties, e.g. text. Then you will
have to use the "instance" property in the selector to pick one of the qualifying instances, like below:
d(text="Add new", instance=0)
In addition, uiautomator2 provides a list-like API (similar to jQuery):
print(d(text="Add new").count)
print(len(d(text="Add new")))
obj = d(text="Add new")[0]
obj = d(text="Add new")[1]
for view in d(text="Add new"):
print(view.info)
Notes: when using selectors in a code block that walks through the result list, you must ensure that the UI elements on the screen
remain unchanged. Otherwise, an Element-Not-Found error could occur when iterating through the list.
Get the selected UI object status and its information
-
Check if the specific UI object exists
if d(text="Settings").exists:
print("Settings button exists")
if d.exists(text="Settings"):
print("Settings button exists")
if d(text="Settings").exists(timeout=3):
print("Settings button appeared within 3 seconds")
-
Retrieve the info of the specific UI object
info = d(text="Settings").info
print(info)
Below is a possible output:
{
"contentDescription": "",
"checked": false,
"scrollable": false,
"text": "Settings",
"packageName": "com.android.launcher",
"selected": false,
"enabled": true,
"bounds": {
"top": 385,
"right": 360,
"bottom": 585,
"left": 200
},
"className": "android.widget.TextView",
"focused": false,
"focusable": true,
"clickable": true,
"childCount": 0,
"longClickable": true,
"visibleBounds": {
"top": 385,
"right": 360,
"bottom": 585,
"left": 200
},
"checkable": false
}
-
Get/Set/Clear text of an editable field (e.g., EditText widgets)
text_content = d(className="android.widget.EditText").get_text()
d(className="android.widget.EditText").set_text("My text...")
d(className="android.widget.EditText").clear_text()
-
Get Widget center point
x, y = d(text="Settings").center()
-
Take screenshot of widget
im = d(text="Settings").screenshot()
im.save("settings.jpg")
Perform the click action on the selected UI object
-
Perform click on the specific object
d(text="Settings").click()
d(text="Settings").click(timeout=10)
d(text="Settings").click(offset=(0.5, 0.5))
d(text="Settings").click(offset=(0, 0))
d(text="Settings").click(offset=(1, 1))
clicked = d(text='Skip').click_exists(timeout=10.0)
is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0)
-
Perform long click on the specific UI object
d(text="Settings").long_click()
d(text="Settings").long_click(duration=1.0)
Gesture actions for the specific UI object
-
Drag the UI object towards another point or another UI object
d(text="Settings").drag_to(x, y, duration=0.5)
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:
left
right
up
(Previously 'top')
down
(Previously 'bottom')
d(text="Settings").swipe("right")
d(text="Settings").swipe("left", steps=10)
d(text="Settings").swipe("up", steps=20)
d(text="Settings").swipe("down", steps=20)
-
Two-point gesture from one pair of points to another (for pinch/zoom)
d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2), steps=100)
-
Two-point gesture on the specific UI object (pinch in/out)
Supports two gestures:
in
: from edge to center (pinch in)
out
: from center to edge (pinch out)
d(text="Settings").pinch_in(percent=100, steps=10)
d(text="Settings").pinch_out(percent=100, steps=10)
-
Wait until the specific UI appears or disappears
appeared = d(text="Settings").wait(timeout=3.0)
if appeared:
print("Settings appeared")
gone = d(text="Settings").wait_gone(timeout=1.0)
if gone:
print("Settings disappeared")
The default timeout is 20s. See Global Settings for more details.
-
Perform fling on the specific UI object (scrollable)
Possible properties:
horizontal
or vertical
(or horiz
, vert
)
forward
or backward
or toBeginning
or toEnd
d(scrollable=True).fling()
d(scrollable=True).fling.horizontal.forward()
d(scrollable=True).fling.vertical.backward()
d(scrollable=True).fling.horizontal.toBeginning(max_swipes=1000)
d(scrollable=True).fling.vertical.toEnd()
-
Perform scroll on the specific UI object (scrollable)
Possible properties:
horizontal
or vertical
(or horiz
, vert
)
forward
or backward
or toBeginning
or toEnd
, or to(selector)
d(scrollable=True).scroll(steps=10)
d(scrollable=True).scroll.horizontal.forward(steps=100)
d(scrollable=True).scroll.vertical.backward()
d(scrollable=True).scroll.horizontal.toBeginning(steps=100, max_swipes=1000)
d(scrollable=True).scroll.vertical.toEnd()
d(scrollable=True).scroll.vertical.to(text="Security")
Input Method (IME)
IME APK: https://github.com/openatx/android-uiautomator-server/releases (Install this for reliable text input)
d.send_keys("Hello123abcEFG")
d.send_keys("Hello123abcEFG", clear=True)
d.clear_text()
d.send_action()
d.hide_keyboard()
When send_keys
is used, it prioritizes using the clipboard for input. If the clipboard interface is unavailable, it will attempt to install and use an auxiliary IME.
print(d.current_ime())
For more, refer to: IME_ACTION_CODE
Toast
last_toast_message = d.toast.get_message(wait_timeout=5, default=None)
print(last_toast_message)
d.toast.reset()
WatchContext (Deprecated)
Note: This interface is not highly recommended. It's better to check for pop-ups before clicking elements.
The current watch_context
uses threading and checks every 2 seconds.
Currently, only click
is a trigger operation.
with d.watch_context() as ctx:
ctx.when("^(立即下载|立即更新)$").when("取消").click()
ctx.when("同意").click()
ctx.when("确定").click()
ctx.wait_stable()
ctx.when("仲夏之夜").call(lambda d: d.press("back"))
ctx.when("确定").call(lambda el: el.click())
with d.watch_context(builtin=True) as ctx:
ctx.when("@tb:id/jview_view").when('//*[@content-desc="图片"]').click()
Alternative way:
ctx = d.watch_context()
ctx.when("设置").click()
ctx.wait_stable()
ctx.start()
ctx.stop()
Global Settings
import uiautomator2 as u2
u2.settings['HTTP_TIMEOUT'] = 60
Other configurations are mostly centralized in d.settings
. Configurations may be added or removed based on future needs.
print(d.settings)
d.settings['operation_delay'] = (0.5, 1)
d.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']
d.settings['wait_timeout'] = 20.0
d.settings['max_depth'] = 50
When settings are deprecated due to version upgrades, a DeprecatedWarning will be shown, but no exception will be raised.
>>> d.settings['click_before_delay'] = 1
UiAutomator timeout settings (hidden methods):
>>> d.jsonrpc.setConfigurator({"waitForIdleTimeout": 100, "waitForSelectorTimeout": 0})
>>> print(d.jsonrpc.getConfigurator())
To prevent client program timeouts, waitForIdleTimeout
and waitForSelectorTimeout
are currently set to 0
by default by uiautomator2 itself (not by the underlying uiautomator server).
Refs: Google uiautomator Configurator
Application Management
This part showcases how to perform app management.
Install Application
We only support installing an APK from a URL or local path.
d.app_install('http://some-domain.com/some.apk')
Start Application
d.app_start("com.example.hello_world")
d.app_start("com.example.hello_world", use_monkey=True)
d.app_start("com.example.hello_world", ".MainActivity")
d.app_start("com.example.hello_world", stop=True)
Stop Application
d.app_stop("com.example.hello_world")
d.app_clear('com.example.hello_world')
Stop All Applications
d.app_stop_all()
d.app_stop_all(excludes=['com.examples.demo'])
Get Application Information
info = d.app_info("com.example.demo")
print(info)
img = d.app_icon("com.example.demo")
if img:
img.save("icon.png")
List All Running Applications
running_apps = d.app_list_running()
print(running_apps)
Wait for Application to Run
pid = d.app_wait("com.example.android")
if not pid:
print("com.example.android is not running")
else:
print(f"com.example.android pid is {pid}")
pid = d.app_wait("com.example.android", front=True)
if pid:
print("com.example.android is in foreground")
pid = d.app_wait("com.example.android", timeout=10.0)
Push and Pull Files
-
Push a file to the device
d.push("foo.txt", "/sdcard/")
d.push("foo.txt", "/sdcard/bar.txt")
import io
with io.BytesIO(b"file content") as f:
d.push(f, "/sdcard/from_io.txt")
d.push("foo.sh", "/data/local/tmp/", mode=0o755)
-
Pull a file from the device
d.pull("/sdcard/tmp.txt", "tmp.txt")
try:
d.pull("/sdcard/some-file-not-exists.txt", "tmp.txt")
except FileNotFoundError:
print("File not found on device")
Other Application Operations
d.open_url("appname://appnamehost")
Session (Beta)
Session represents an app lifecycle. Can be used to start app, detect app crash.
-
Launch and close app
sess = d.session("com.netease.cloudmusic")
sess.close()
-
Use python with
to launch and close app
with d.session("com.netease.cloudmusic") as sess:
pass
-
Attach to the running app
sess = d.session("com.netease.cloudmusic", attach=True)
-
Detect app crash
if sess.running():
print("Session is active")
else:
print("Session is not active (app might have crashed or closed)")
Other APIs
Stop Background HTTP Service
Normally, when the Python program exits, the UiAutomator service on the device also exits.
However, you can also stop the service via an API call.
d.uiautomator.stop()
Enable Debugging
Print out the HTTP request information behind the code.
>>> d.debug = True
>>> print(d.info)
For more structured logging:
import logging
from uiautomator2 import set_log_level
set_log_level(logging.DEBUG)
Command Line Functions
$device_ip
represents the device's IP address.
To specify a device, pass --serial
, e.g., python -m uiautomator2 --serial bff1234 <SubCommand>
. SubCommand can be screenshot
, current
, etc.
1.0.3 Added: python -m uiautomator2
is equivalent to uiautomator2
-
screenshot
: Take a screenshot
uiautomator2 screenshot screenshot.jpg
uiautomator2 --serial <YOUR_DEVICE_SERIAL> screenshot screenshot.jpg
-
current
: Get current package name and activity
uiautomator2 current
-
uninstall
: Uninstall app
uiautomator2 uninstall <package-name>
uiautomator2 uninstall <package-name-1> <package-name-2>
-
stop
: Stop app
uiautomator2 stop com.example.app
-
doctor
: Check uiautomator2 environment
uiautomator2 doctor
-
install
: Install APK from URL or local path
uiautomator2 install http://example.com/app.apk
uiautomator2 install /path/to/local/app.apk
-
clear
: Clear app data
uiautomator2 clear <package-name>
-
start
: Start app
uiautomator2 start <package-name>
uiautomator2 start <package-name>/<activity-name>
-
list
: List connected devices
uiautomator2 list
-
version
: Show uiautomator2 version
uiautomator2 version
Differences between Google UiAutomator 2.0 and 1.x
Reference: https://www.cnblogs.com/insist8089/p/6898181.html (Chinese)
- New APIs: UiObject2, Until, By, BySelector (in UiAutomator 2.x Java library)
- Import Style: In 2.0,
com.android.uiautomator.core.*
is deprecated. Use android.support.test.uiautomator.*
(now androidx.test.uiautomator.*
).
- Build System: Maven and/or Ant (1.x); Gradle (2.0).
- Test Package Format: From zip/jar (1.x) to APK (2.0).
- Running Tests via ADB:
- 1.x:
adb shell uiautomator runtest UiTest.jar -c package.name.ClassName
- 2.0:
adb shell am instrument -w -r -e debug false -e class package.name.ClassName#methodName package.name.test/androidx.test.runner.AndroidJUnitRunner
- Access to Android Services/APIs: 1.x: No; 2.0: Yes.
- Log Output: 1.x:
System.out.print
echoes to the execution terminal; 2.0: Output to Logcat.
- Execution: 2.0: Test cases do not need to inherit from any parent class, method names are not restricted, uses Annotations; 1.x: Needs to inherit
UiAutomatorTestCase
, test methods must start with test
.
(Note: uiautomator2 Python library abstracts away many of these Java-level differences, but understanding the underlying UiAutomator evolution can be helpful.)
Dependent Projects
Contributors
contributors
Other Excellent Projects
(Order matters, additions welcome)
LICENSE
MIT