Install
openclaw skills install hospital-android-adbAutomate hospital appointment booking on the 医联 (Yilian) Android app using uiautomator2. Covers hospital search (with Chinese text input), department selection, date/time slot selection, and confirmation flow. Uses uiautomator2 only — no Appium needed.
openclaw skills install hospital-android-adbAutomate doctor appointment booking on the 医联 Android app (com.wonders.mobile.app.yilian). The app uses native Android views (no UC WebView issues). Use uiautomator2 for all interactions — Appium is NOT needed.
import uiautomator2 as u2
d = u2.connect()
Kill 12306 first if running — both apps can't share the accessibility service:
import subprocess
subprocess.run(["adb", "shell", "am", "force-stop", "com.MobileTicket"])
Launch 医联:
subprocess.run(["adb", "shell", "monkey", "-p", "com.wonders.mobile.app.yilian",
"-c", "android.intent.category.LAUNCHER", "1"])
import time; time.sleep(5)
| Field | Value |
|---|---|
| Package | com.wonders.mobile.app.yilian |
| Activity chain | MainActivity → SearchActivity → HospitalHomePageActivity → DepartmentActivity → SpecialDiseaseActivity → SchedulingActivity → ReserveInfoActivity |
| UI type | Native Android (RecyclerView, LinearLayout, TextView) — no WebView issues |
| Input method | el.set_text("华山医院") via AccessibilityService — no IME needed |
set_text()d.send_keys("华山医院") fails (clipboard permission denied). adb shell input text fails (NullPointerException on Chinese chars). Use set_text() instead:
# Focus the field, then set text via AccessibilityService ACTION_SET_TEXT
el = d(className='android.widget.EditText')
el.click() # focus first
time.sleep(0.5)
el.set_text("华山医院") # bypasses IME/clipboard entirely
This works because set_text() uses the Android AccessibilityService's ACTION_SET_TEXT action, which directly injects text into the focused EditText without needing an IME or clipboard permissions.
Many department/button TextViews have clickable=false. Their parent container (LinearLayout/FrameLayout) has clickable=true. Always check parent containers by bounds containment:
for p in root.iter('node'):
if p.get('clickable') == 'true' and parent_bounds_contains(p, child_bounds):
# This is the actual clickable target
The SchedulingActivity`` RecyclerView takes **2-10 seconds** to populate with available dates. After tapping a slot, poll the dump for date texts like "2026-06-01"` until they appear.
xml = d.dump_hierarchy()
root = __import__('xml.etree.ElementTree', fromlist=['ElementTree']).fromstring(xml.encode())
import re
for node in root.iter('node'):
text = node.get('text', '').strip()
bounds = node.get('bounds', '')
clickable = node.get('clickable')
if text and clickable == 'true':
nums = re.findall(r'\d+', bounds)
if len(nums) >= 4:
h = int(nums[3]) - int(nums[1])
if h > 20: # meaningful height
print(f"'{text}' at {bounds}")
def find_clickable_parent(root, child_bounds):
child_nums = __import__('re').findall(r'\d+', child_bounds)
if len(child_nums) < 4: return None
cx1, cy1, cx2, cy2 = int(child_nums[0]), int(child_nums[1]), int(child_nums[2]), int(child_nums[3])
for p in root.iter('node'):
if p.get('clickable') != 'true': continue
pn = __import__('re').findall(r'\d+', p.get('bounds', ''))
if len(pn) >= 4:
px1, py1, px2, py2 = int(pn[0]), int(pn[1]), int(pn[2]), int(pn[3])
if px1 <= cx1 and py1 <= cy1 and px2 >= cx2 and py2 >= cy2:
return p
return None
d.screenshot('/tmp/screen.png')
import pytesseract
from PIL import Image
text = pytesseract.image_to_string(Image.open('/tmp/screen.png'),
lang='chi_sim', config='--psm 6')
Home page UI:
d(text='搜医院、科室、医生').click()
time.sleep(2)
On the search page, type the hospital name:
el = d(className='android.widget.EditText')
el.click()
time.sleep(0.5)
el.set_text("华山医院") # set text via AccessibilityService
time.sleep(2) # wait for suggestions
After typing, the search results / suggestions appear. Tap the hospital card:
xml = d.dump_hierarchy()
if '华山医院' in xml:
# Tap at hospital card position
d.click(540, 1033) # center of hospital card
On the search result page, tap the hospital card to go to its detail page:
# Look for the hospital card in the results
for node in root.iter('node'):
if '华山医院' in node.get('text', ''):
parent = find_clickable_parent(root, node.get('bounds', ''))
# Tap parent's center
...
This navigates to HospitalHomePageActivity.
On the hospital detail page (HospitalHomePageActivity), there are icon buttons:
# The "预约挂号" icon is in the clickable container:
# Row 1: [0,1279][270,1522], [270,1279][540,1522], [540,1279][810,1522], [810,1279][1080,1522]
# Column 2 (270-540) = 预约挂号
d.click(405, 1400) # center of second icon
This navigates to DepartmentActivity.
On DepartmentActivity:
First select the appointment type tab, then tap the target department:
# Tap "普通门诊" tab
d.click(900, 516) # center of third tab
# The department list refreshes. Now find "皮肤科":
xml = d.dump_hierarchy()
root = __import__('xml.etree.ElementTree', fromlist=['ElementTree']).fromstring(xml.encode())
for node in root.iter('node'):
if node.get('text', '').strip() == '皮肤科':
parent = find_clickable_parent(root, node.get('bounds', ''))
if parent:
nums = __import__('re').findall(r'\d+', parent.get('bounds', ''))
cx = (int(nums[0]) + int(nums[2])) // 2
cy = (int(nums[1]) + int(nums[3])) // 2
d.click(cx, cy)
This may navigate to SpecialDiseaseActivity (department detail).
On SpecialDiseaseActivity, find the appointment type entry for the department:
# Look for entries like "普通门诊-[医联平台] ¥25" with "有号" (available)
for node in root.iter('node'):
t = node.get('text', '').strip()
if '有号' in t or '普通门诊' in t:
parent = find_clickable_parent(root, node.get('bounds', ''))
if parent:
# Tap to navigate to SchedulingActivity
On SchedulingActivity (activity == ".patient.ui.hospital.SchedulingActivity"):
The RecyclerView takes 2-10 seconds to populate. Poll for data:
# Wait for schedule data to load
for i in range(15):
time.sleep(1)
xml = d.dump_hierarchy()
if '2026-06-01' in xml: # target date appeared
break
# Target date will show as: "2026-06-01 星期一 上午"
# Find its clickable parent and tap
root = __import__('xml.etree.ElementTree', fromlist=['ElementTree']).fromstring(xml.encode())
for node in root.iter('node'):
if node.get('text', '').strip() == '2026-06-01 星期一 上午':
parent = find_clickable_parent(root, node.get('bounds', ''))
...
This navigates to ReserveInfoActivity.
On ReserveInfoActivity, verify all details:
Patient selection: tap the patient name to select/deselect. Captcha section:
SMS verification code is required — inform the user to check their phone.
# Tap 确定预约 after captcha is entered
d.click(540, 2292)
| # | Activity | What You See |
|---|---|---|
| 1 | .patient.ui.MainActivity | Home page: search box, 当日挂号 |
| 2 | .patient.ui.hospital.search.SearchActivity | Hospital search results / suggestions |
| 3 | .patient.ui.hospital.HospitalHomePageActivity | Hospital detail: 预约挂号 icon |
| 4 | .patient.ui.hospital.depart.DepartmentActivity | Department list with tabs |
| 5 | .patient.ui.hospital.SpecialDiseaseActivity | Department detail: appointment type entry |
| 6 | .patient.ui.hospital.SchedulingActivity | Date/time slot picker (RecyclerView) |
| 7 | .patient.ui.hospital.ReserveInfoActivity | Confirmation: details, captcha, 确定预约 |
| Element | Where | Notes |
|---|---|---|
| 搜医院、科室、医生 | Home page | Clickable search box |
| 预约挂号 (icon) | Hospital page | 2nd icon in row 1 |
| 当日挂号 | Hospital page | 3rd icon in row 1 |
| 普通门诊 | Department page | Tab, 3rd position |
| 皮肤科 / other dept | Department page | In scroll list |
| 有号 | Various | Indicates availability |
| 确定预约 | Reserve page | Submit button |
| 获取验证码 | Reserve page | Request SMS |
| 请输入验证码 | Reserve page | Captcha input |
adb kill-server && adb start-server to recover.d.dump_hierarchy() + check activity:
├─ activity == "MainActivity" → HOME
│ → tap search box → tap hot suggestion
├─ activity == "SearchActivity" → SEARCH RESULTS
│ → tap hospital card
├─ activity == "HospitalHomePageActivity" → HOSPITAL PAGE
│ → tap 预约挂号 icon (2nd icon, 1st row)
├─ activity == "DepartmentActivity" → DEPARTMENTS
│ → select tab (普通门诊) → tap department name
├─ activity == "SpecialDiseaseActivity" → DEPT DETAIL
│ → tap available appointment entry (有号)
├─ activity == "SchedulingActivity" → DATE PICKER
│ → wait for data → tap target date/time slot
├─ activity == "ReserveInfoActivity" → CONFIRMATION
│ → verify details → get SMS captcha → tap 确定预约
└─ otherwise → unknown state, take screenshot
uiautomator2 Python library only (no Appium)