Skip to content
Back to skills

12306 Android Adb

ASecurity

12306-specific knowledge for booking train tickets via the Android app. Covers UC WebView virtual list behavior, proven booking flow, and common pitfalls. Uses appium-android-adb bridge OR direct uiautomator2 for all interaction.

  • 2 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added September 9, 2026
toolspythongoshellbash

Works with

  • cli

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add Lord1Egypt/awesome-skill-forge --skill 12306-android-adb --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 12306 Android Adb?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 12306 Android Adb
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lord1egypt-12306-android-adb/badge)](https://www.skillsdirectory.com/skills/lord1egypt-12306-android-adb)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: 12306-android-adb
description: 12306-specific knowledge for booking train tickets via the Android app. Covers UC WebView virtual list behavior, proven booking flow, and common pitfalls. Uses appium-android-adb bridge OR direct uiautomator2 for all interaction.
---

# 12306 Train Booking (Android)

12306-specific knowledge, workflows, and pitfalls. The app uses Alipay Nebula UC WebView for the train list and booking pages, which has unique automation challenges.

## 🚨 Before Any 12306 Action

Ensure the app is running and on the correct page:

```bash
# Start Appium bridge (if using bridge_daemon.py approach):
bash ~/.openclaw/workspace/skills/appium-android-adb/start_bridge.sh

# Or use uiautomator2 directly (more reliable for WebView):
python3 -c "import uiautomator2 as u2; d = u2.connect(); d.app_start('com.MobileTicket')"
```

## ⚠️ Critical: UC WebView Virtual List Behavior

The train list uses a **virtual list** inside the UC WebView. This has extreme implications:

### What DOES work
| Method | Target | Reliability |
|--------|--------|-------------|
| `uiautomator2 text('预订').click()` | "预订" book buttons | ✅ Always works (proper persistent bounds) |
| `uiautomator2 text('查询车票').click()` | Native query button | ✅ Always works |
| `uiautomator2 text('选择乘车人').click()` | Native passenger selector | ✅ Always works |
| `uiautomator2.click(x, y)` on native views | Date tabs, bottom buttons | ✅ Always works |
| Screenshot + OCR + ADB tap | Any visible element | ✅ Works with good OCR |
| `Appium element.click()` on "预订" buttons | "预订" with proper bounds | ✅ Works |

### What DOES NOT work
| Method | Target | Why |
|--------|--------|-----|
| Clicking collapsed train name text | Train entry at y=407 or y=2255 (h=6) | Virtual list collapses ALL non-current items |
| ADB `input tap` | Any UC WebView content | UC WebView filters synthetic touch events |
| W3C Actions swipe | UC WebView scroll | Touch events filtered |
| `mobile: scrollGesture` | UC WebView element | Returns False consistently |
| `d.textContains('G 7 0 0 4').click()` | Train name in collapsed zone | Multiple trains share collapsed bounds |
| `tap_bounds` on collapsed element | Train name at y=2255 with h=6 | Event goes to WebView's virtual position, not visual |

### Key Insight: "预订" Buttons Have Proper Bounds

The ONLY reliable click targets in the UC WebView train list are the **"预订" action buttons**. Unlike the text content (train name, times, seat info) which gets collapsed by the virtual list, the "预订" button is:
- Always rendered with proper bounds (h=87px)
- Clickable via accessibility service
- Positioned consistently ~195px below each train card's top
- Unique per train card (find the one nearest your target train's y-position)

## Booking Flow

### Step 1: Home Page — Search Trains

```
dump the page → check for "查询车票" button
If on MainActivity:
  → tap "查询车票" (native button, always works)
  → wait for train list to load (H5Activity appears)
If on H5Activity (already on train list):
  → proceed to Step 2
```

### Step 2: Find Target Train's "预订" Button

**DO NOT try to click the train name or text content.** Instead:

1. **Scroll** the list until you see trains near your target departure time:
   ```python
   d.swipe_ext('up', scale=0.3)   # scroll forward
   d.swipe_ext('down', scale=0.3)  # scroll backward
   ```

2. **Check for G7004 in the DOM** (even collapsed):
   ```python
   xml = d.dump_hierarchy()
   if 'G 7 0 0 4' in xml:
       # G7004 is in the list — look for its "预订" button
       pass
   ```

3. **Find the "预订" button** associated with your train. "预订" buttons appear at regular y-intervals (~315px apart). The one closest to your target train's y-position belongs to that train:
   ```python
   # Get all "预订" buttons with positions
   buttons = d(text='预订')
   for btn in buttons:
       pos = btn.info['bounds']
       y_center = (pos['top'] + pos['bottom']) // 2
       print(f"预订 at y={y_center}")

   # G7004 at y=1793 → the "预订" at y=1988 is its book button
   ```

4. **Click "预订":**
   ```python
   d(text='预订').click()          # clicks first one
   # OR click specific one by position:
   for btn in d(text='预订'):
       if 1900 < btn.info['bounds']['top'] < 2100:
           btn.click()
           break
   ```

### Step 2b: OCR-Based Fallback (if DOM is unreliable)

If the WebView's DOM is too collapsed to find the right "预订":

```python
import subprocess, pytesseract
from PIL import Image

# Take screenshot
subprocess.run(['adb', 'shell', 'screencap', '-p', '/sdcard/screen.png'], timeout=10)
subprocess.run(['adb', 'pull', '/sdcard/screen.png', '/tmp/screen.png'], timeout=10)

# OCR to find departure time
data = pytesseract.image_to_data(Image.open('/tmp/screen.png'),
    lang='chi_sim+eng', output_type=pytesseract.Output.DICT, config='--psm 6')

# Find "08:00" departure text on LEFT side (x < 300)
for i in range(len(data['text'])):
    if '08:00' in data['text'][i] and int(data['left'][i]) < 300:
        y = data['top'][i]
        # Tap "预订" position (~195px below departure time)
        import subprocess
        subprocess.run(['adb', 'shell', 'input', 'tap', '939', str(y + 195)])
        break
```

### Step 3: Quiet Carriage Dialog (if appears)

After clicking "预订", a dialog may appear:
- "选择该车次二等座可能候补到静音车厢席位,是否继续选择?" → tap "继续" or "选择"
- "温馨提示" with quiet carriage rules → tap "选择" (bottom-right button)

Use OCR to find the confirm button position, then ADB tap it.

### Step 4: Select Passenger

```
On order page, verify:
  → G7004 train info displayed
  → 二等座 selected (default)
  → Price ¥41 shown

If no passenger selected:
  → tap "选择乘车人" (native, works)
  → tap passenger name (e.g. "熊子慧")
  → tap "确认" (bottom button)
```

### Step 5: Submit Order

```
tap "提交订单" button
  → verify: order submission confirmation
  → if "温馨提示" with "车票信息已过期" → go back, re-search
  → if "立即支付" → proceed to payment
```

### Step 6: Payment (manual)

```
"立即支付" or "去支付" appears → user handles Alipay/WeChat
```

## Train List Navigation (Scrolling)

The UC WebView's virtual list has three zones:
- **Top collapsed** (y=407, h=6): Items scrolled past the top
- **Proper viewport** (y=500-2200, h=60-130): Currently visible items with real bounds
- **Bottom collapsed** (y=2255, h=6): Items below the viewport

Each scroll (scale=0.3) advances the list by approximately 2 train cards (~30-60 min of departures). To reach a specific train:
1. Start from the earliest visible time
2. Calculate approximate scrolls needed: `(target_hour - current_hour) * 60 / 15` (each scroll ≈ 15 min of departures)
3. Scroll, then check for target text in DOM (even collapsed)
4. Once target text appears in DOM, look for its "预订" button

## Decision Tree

```
dump / check current page:
  ├─ package != "com.MobileTicket"
  │   → app not running, start it
  ├─ ".ui.activity.MainActivity" in activity
  │   → HOME PAGE
  │   → verify stations are set → tap "查询车票"
  ├─ "H5Activity" in activity
  │   ├─ "提交订单" in text → ORDER CONFIRMATION
  │   │   → select passenger if needed → submit
  │   ├─ "预订" in text → TRAIN LIST (with "预订" buttons)
  │   │   → find target train → click its "预订" button
  │   ├─ "次列车" in text → TRAIN LIST (loading)
  │   │   → scroll until target appears → find "预订"
  │   └─ "立即支付" in text → PAYMENT
  │       → user handles payment
  └─ anything else → not 12306, investigate
```

## Common Pitfalls

1. **NEVER click collapsed train names** — they all share the same y=407 or y=2255 bounds. Click the "预订" button instead.
2. **"预订" buttons have proper bounds** — they're the only reliable WebView targets.
3. **Screen may auto-lock** — keep device awake: `adb shell svc power stayon true`
4. **Session conflicts** — Appium and uiautomator2 can't run simultaneously (both need AccessibilityService).
5. **Page may time out** — if no action for ~5 min, the list might need re-querying.
6. **Date tabs are native** — tapping "05月31日 周日" at bounds [189,257][312,383] always works.
7. **Train numbers have spaces** — search for `G 7 0 0 4` not `G7004`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…