Metadata-Version: 2.4
Name: dbgidchromium
Version: 1.1.10
Summary: Android-focused browser automation toolkit
Home-page: https://github.com/dbgid/dbgidchromium
Author: dbgid
License: MIT
Project-URL: Repository, https://github.com/dbgid/dbgidchromium
Project-URL: Required Browser, https://github.com/dbgid/DBG-ID-Browser
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: author
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-python
Dynamic: summary

# dbgidchromium

**Version:** `1.1.9`

Android-focused browser automation toolkit for **[DBG ID Browser](https://github.com/dbgid/DBG-ID-Browser)** (`com.dbgid.browser`).

Python talks to the Android browser over a local socket. Protocol values are base64-encoded (request and response), similar to Selenium-style WebDriver APIs with extra Playwright-style locators, network capture, header spoofing, and session resume helpers.

| | |
|---|---|
| Package | `dbgidchromium` |
| Version | **1.1.9** |
| Python | `>= 3.10` |
| License | MIT |
| Browser package | `com.dbgid.browser` |
| Source API | `__init__.py` |
| Alias | `Chrome = WebDriver`, `AsyncChrome = AsyncWebDriver` |

```python
import dbgidchromium as chromium
print(chromium.__version__)  # 1.1.9
```

---

## Requirements

1. **DBG ID Browser** installed on the Android device/emulator.
2. Shell access able to start activities/services (`am` / `cmd activity`), preferably via **Shizuku `rish`** on Termux.
3. Python 3.10+ (stdlib only; no extra pip deps required for core).

Optional env:

| Env | Purpose |
|-----|---------|
| `DBGIDCHROMIUM_RISH` / `DBGID_RISH` | Path to `rish` (default `~/bin/rish`) |
| `DBGIDCHROMIUM_ANDROID_SHELL` / `DBGID_ANDROID_SHELL` | `auto` (default), `rish`, `direct` |
| `DBGIDCHROMIUM_GUI_FALLBACK` / `DBGID_GUI_FALLBACK` | Allow GUI fallback when headless launch fails |

---

## Install

```bash
# from source (this repo root)
pip install -e .

# or plain path import
export PYTHONPATH=/path/to/dbgidchromium/..
python -c "import dbgidchromium; print(dbgidchromium.__version__)"
```

Packaging maps the repo root as package `dbgidchromium` and ships `ip2asn-v4-u32.tsv` for IP/ASN spoof helpers.

---

## Quick start

```python
import dbgidchromium as chromium
from dbgidchromium import By, Keys, WebDriverWait, presence_of_element_located

driver = chromium.WebDriver(gui=False, debug=True)
try:
    driver.goto(
        "https://example.com",
        timeout=45,
        wait_until="javascript",
        clear_browser_state=True,
        log=True,
    )
    print(driver.title)
    print(driver.current_url)

    el = driver.find_element(By.CSS_SELECTOR, "h1")
    print(el.text)

    driver.locator("a").first.click()
finally:
    driver.close()
```

Context manager:

```python
with chromium.WebDriver(gui=False) as driver:
    driver.goto("https://example.com", wait_until="complete")
    print(driver.page_source[:200])
```

Alias:

```python
from dbgidchromium import Chrome  # same as WebDriver
driver = Chrome(gui=True)
```

---

## Architecture (short)

```
Python (dbgidchromium)
    │  TCP socket + base64 DictMap payload
    ▼
DBG ID Browser (Android WebView / service)
    SplashActivity  → GUI mode
    MainService     → headless / gui=False
```

- `WebDriver` subclasses `RemoteConnection` (a listening `socket.socket`).
- Launch builds a `webdriver://com.dbgid.browser/?data=<base64-json>` intent.
- Commands are string names from `Command` (`"get"`, `"find element"`, …).
- `detach()` keeps the browser open; `close()` ends the WebDriver session.

---

## Module constants

| Name | Value / meaning |
|------|-----------------|
| `__version__` | `"1.1.9"` |
| `ANDROID_PACKAGE` | `"com.dbgid.browser"` |
| `ANDROID_ACTIVITY` | `".SplashActivity"` |
| `ANDROID_SERVICE` | `".MainService"` |
| `EXECUTE_SCRIPT_RESULT_ATTR` | DOM attr for script result side-channel |
| `EXECUTE_SCRIPT_STATUS_ATTR` | DOM attr for script status |
| `EXECUTE_SCRIPT_REQUEST_ATTR` | DOM attr for script request id |
| `REQUEST_INTERCEPTOR_WINDOW_NAME_PREFIX` | Prefix used by request interceptor |
| `REQUEST_INTERCEPTOR_DEFAULT_TYPES` | `("image", "audio", "video")` |
| `REQUEST_INTERCEPTOR_TYPE_ALIASES` | Maps aliases (`css`→`stylesheet`, `xhr`→`connect`, …) |
| `IP2ASN_DB_FILE` | Path to bundled `ip2asn-v4-u32.tsv` |
| `no_encode_again` | Global encode flag (default `False`) |
| `Chrome` | Alias of `WebDriver` |
| `AsyncChrome` | Alias of `AsyncWebDriver` |

---

## Exceptions

All extend `WebDriverException` except the base itself.

| Class | When |
|-------|------|
| `WebDriverException` | Generic driver/protocol error |
| `NoSuchElementException` | Element not found |
| `ApplicationClosed` | Browser/app closed while talking |
| `CrossOriginFrameException` | Cross-origin iframe DOM access blocked |
| `InvalidElementStateException` | Element state not valid for action |
| `UnexpectedTagNameException` | Wrong tag (e.g. `Select` on non-`<select>`) |

```python
from dbgidchromium import NoSuchElementException, By

try:
    driver.find_element(By.CSS_SELECTOR, "#missing")
except NoSuchElementException as e:
    print("not found:", e)
```

---

## `By` — locator strategies

```python
class By:
    ID = "id"
    XPATH = "xpath"
    LINK_TEXT = "link text"
    PARTIAL_LINK_TEXT = "partial link text"
    NAME = "name"
    TAG_NAME = "tag name"
    CLASS_NAME = "class name"
    CSS_SELECTOR = "css selector"
```

```python
driver.find_element(By.CSS_SELECTOR, "#login")
driver.find_elements(By.XPATH, "//button")
driver.find_element(By.NAME, "email")
driver.find_element(By.CLASS_NAME, "btn-primary")
driver.find_element(By.TAG_NAME, "h1")
driver.find_element(By.LINK_TEXT, "Sign in")
driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")
```

### Important: `By.ID` limitation

On some DBG ID Browser builds, `By.ID` may return empty because the browser side treats the value like a bare `querySelector` string instead of `getElementById` / `#id`.

**Workaround — prefer CSS with `#`:**

```python
# may fail on affected browser builds
driver.find_element(By.ID, "text-input")

# reliable
driver.find_element(By.CSS_SELECTOR, "#text-input")
driver.query_selector("#text-input")
```

---

## `Keys`

Android keycodes used with `send_key` / locator `press`:

```python
class Keys:
    ENTER = 66
    TAB = 61
```

```python
el.send_key(Keys.ENTER)
driver.locator("#q").press("Enter")  # locator path
```

---

## `Command` — wire command names

Low-level protocol action strings. Usually you call `WebDriver` methods instead of building these by hand.

| Attribute | Wire value |
|-----------|------------|
| `CLOSE` | `close` |
| `GET` | `get` |
| `FIND_ELEMENT` / `FIND_ELEMENTS` | `find element` / `find elements` |
| `CLICK_JAVA` | `click java` |
| `EXECUTE_SCRIPT` | `execute script` |
| `GET_ATTRIBUTE` / `SET_ATTRIBUTE` / `REMOVE_ATTRIBUTE` | attribute ops |
| `SEND_KEY` / `SEND_TEXT` | input |
| `PAGE_SOURCE` / `TITLE` / `CURRENT_URL` | page meta |
| `GET_COOKIE(S)` / `SET_COOKIE` / `CLEAR_COOKIE(S)` / `DELETE_ALL_COOKIE` | cookies |
| `GET/SET_LOCAL_STORAGE` / `GET/SET_SESSION_STORAGE` / clears | storage |
| `GET/SET_USER_AGENT` / `GET/SET_HEADERS` | identity |
| `SET_PROXY` / `SET_REQUEST_INTERCEPTOR` | network |
| `SWIPE` / `SWIPE_UP` / `SWIPE_DOWN` / `SCROLL_TO` | gestures |
| `WINDOW_HANDLES` / `CURRENT_WINDOW_HANDLE` / `SWITCH_TO_WINDOW` | tabs |
| `SCREENSHOT` | screenshot |
| `WAIT_UNTIL_ELEMENT` / `WAIT_UNTIL_NOT_ELEMENT` | waits |
| `GET_RECAPTCHA_V3_TOKEN` | reCAPTCHA v3 |
| `OVERRIDE_JS_FUNCTION` | inject/override JS |
| `INIT` | session bootstrap |

---

## `DictMap` / encode helpers

Protocol dict that stringifies values in the browser’s expected form.

```python
from dbgidchromium import DictMap, b64encode, b64decode, decode_data

payload = DictMap({"command": "get", "url": "https://example.com"})
# WebDriver uses DictMap internally when sending commands

print(b64encode("hello"))
print(b64decode(b64encode("hello")))
```

| Function | Role |
|----------|------|
| `b64encode(value)` | Encode str/value → base64 text (with decode_data cleanup) |
| `b64decode(value)` | Decode base64; coerces `true`/`false`/`null`/JSON-ish |
| `decode_data(data)` | Unicode-escape / HTML-unescape cleanup |

---

## `RemoteConnection`

Base of `WebDriver`. Owns the listening socket server the Android app connects to.

```python
# constructed via WebDriver(...); rarely used alone
# __init__(accept_time_out)
# init_socket_server()
```

---

# `WebDriver` (core class)

```python
WebDriver(
    gui=True,
    pip_mode=False,
    lang="en",
    debug=False,
    accept_time_out=60,
    recv_time_out=60 * 60,
    gui_fallback=None,
    resume=False,
)
```

| Parameter | Default | Meaning |
|-----------|---------|---------|
| `gui` | `True` | `True` → Activity UI; `False` → headless service path |
| `pip_mode` | `False` | Picture-in-picture related launch flag |
| `lang` | `"en"` | Browser language hint |
| `debug` | `False` | Extra debug prints |
| `accept_time_out` | `60` | Seconds to accept Android socket connect |
| `recv_time_out` | `3600` | Socket recv timeout |
| `gui_fallback` | env / auto | If headless fails, may fall back to GUI |
| `resume` | `False` | Attach to existing live tab (see session resume) |

### Instance attributes (notable)

| Attribute | Meaning |
|-----------|---------|
| `gui` / `requested_gui` | Effective / requested GUI mode |
| `debug` | Debug flag |
| `switch_to` | `SwitchTo` helper (`driver.switch_to.frame(...)`) |
| `accept_time_out` / `recv_time_out` | Timeouts |
| `_frame_stack` | Nested iframe context stack (internal) |

Supports `with` (`__enter__` / `__exit__` → `close()`).

---

## Lifecycle

| Method | Behavior |
|--------|----------|
| `close()` | End WebDriver session (sends close; tears down connection) |
| `detach()` | Drop Python socket **without** closing Android browser/tab |
| `resume_browser_session(...)` / `resume_chrome()` | New Python driver attached to live browser tab |
| `continue_browser_session()` / `continue_chrome()` | Continue helpers |
| `detach_browser_session(driver)` | Module helper around detach |

```python
driver = chromium.WebDriver(gui=True)
driver.goto("https://example.com")
driver.detach()   # leave tab open

# later, new process/shell:
driver = chromium.resume_browser_session(gui=False, debug=True)
print(driver.current_url)
driver.close()
```

Static Android helpers:

```python
WebDriver.force_stop_command(package)  # force-stop package string builder
WebDriver.home_command()               # home intent helper
WebDriver.wait(delay)                  # sleep wrapper
```

Module launch helpers:

```python
from dbgidchromium import (
    start_browser_activity,
    start_browser_service,
    foreground_browser,
    screencap_png,
)

start_browser_activity(data=None, wait=True, ...)   # am start Activity
start_browser_service(data=None, ...)               # headless MainService
foreground_browser()                                # bring UI forward, keep tabs
png_bytes = screencap_png("/sdcard/shot.png")       # Android display PNG
```

---

## Navigation

### `get(url, **kwargs)`

Low-level navigate (no full wait pipeline of `goto`).

Optional kwargs:

- `use_cookie_from_requests` — load cookies before GET
- `custom_headers` / `headers` — apply headers before GET

### `goto(url, **kwargs)` — preferred

High-level navigation + wait + optional spoof/headers/interceptor/clear.

| Kwarg | Default | Meaning |
|-------|---------|---------|
| `wait_until` | `"javascript"` (GUI); headless defaults differ | Readiness mode |
| `timeout` | `30` | Max wait seconds |
| `poll_frequency` | `~0.06` headless / `~0.12` GUI | Poll interval |
| `settle_time` | `~0.15` headless / `~0.8` GUI | Extra settle after ready |
| `locator` / `wait_for` | `None` | Also wait for element |
| `clear_browser_state` | `True` | `clear_browser()` before first nav |
| `use_spoof` | `None` | IP/ASN header spoof (`True` = random) |
| `custom_headers` / `headers` | `None` | Extra HTTP headers |
| `interceptor` / `interceptor_mime_types` / `block_mime_types` | `None` | Block resource types |
| `fallback_wait_until` | `None` | Softer wait if primary times out |
| `fallback_after` | `None` | When to try fallback |
| `log` | `True` | Log wait progress |

**`wait_until` modes:**

| Value | Meaning |
|-------|---------|
| `url_only` | URL changed / minimal |
| `domcontentloaded` | DOMContentLoaded-ish readiness |
| `realtime` | Early interactive/render signal |
| `javascript` | JS-ready (default for GUI) |
| `complete` / `load` | Full load style readiness |

Headless (`gui=False`) without explicit `wait_until` defaults to `url_only`, or `domcontentloaded` if a locator is passed.

```python
driver.goto(
    "https://www.google.com",
    wait_until="javascript",
    timeout=45,
    poll_frequency=0.10,
    settle_time=0.35,
    clear_browser_state=True,
    use_spoof=True,
    interceptor=["image", "font", "media"],
    custom_headers={"Accept-Language": "en-US"},
    log=True,
)
```

Related:

```python
driver.go_back()
driver.go_forward()
driver.reload()
driver.set_content("<html><body>hi</body></html>", wait_until="javascript", timeout=30)
html = driver.content()          # alias-style page HTML helper
src = driver.page_source         # property
title = driver.title             # property
url = driver.current_url         # property
```

### SPA wait

```python
details = driver.wait_for_spa_ready(
    timeout=30,
    selector="#app",
    min_html_length=1200,
    poll_frequency=0.1,
    stable_time=0.35,
)
# aliases: wait_for_react_ready, wait_for_nextjs_ready (on async wrapper too)
```

```python
driver.wait_for_navigation(
    locator=(By.CSS_SELECTOR, "h1"),
    timeout=30,
    poll_frequency=0.1,
    settle_time=0.75,
    wait_until="javascript",
)
```

---

## Finding elements

### Classic Selenium-style

```python
el = driver.find_element(By.CSS_SELECTOR, "#email")
els = driver.find_elements(By.CSS_SELECTOR, "input")

driver.find_element_by_css_selector("#email")
driver.find_element_by_xpath("//input[@name='q']")
driver.find_element_by_name("q")
driver.find_element_by_class_name("btn")
driver.find_element_by_tag_name("button")
driver.find_element_by_link_text("Docs")
driver.find_element_by_partial_link_text("Doc")
driver.find_element_by_id("email")  # prefer CSS #email if By.ID flaky

driver.query_selector("#email")       # CSS
driver.querySelector("#email")        # camelCase alias
driver.query_selector_all(".item")
driver.querySelectorAll(".item")
```

### Playwright-style locators

```python
loc = driver.locator("css=button.submit")   # or plain CSS / xpath=... / text=...
loc = driver.locator("#email")
loc = driver.locator("xpath=//button")
loc = driver.locator("text=Sign in")
loc = driver.locator("//button")            # xpath if starts with // or (

loc.click()
loc.fill("user@example.com")
loc.type("slow text")
loc.press("Enter")
loc.dblclick()
loc.hover()
loc.check() / loc.uncheck() / loc.set_checked(True)
loc.is_checked() / loc.is_visible() / loc.is_enabled()
loc.text_content() / loc.inner_text() / loc.inner_html()
loc.input_value()
loc.get_attribute("href")
loc.evaluate("el => el.tagName")
loc.wait_for(state="visible", timeout=30)   # visible / attached-style states
loc.count()
loc.all()
loc.nth(0) / loc.first / loc.last
loc.filter(has_text="Save")
loc.locator(".child")                       # nested
loc.all_text_contents()
el = loc.element_handle()                   # -> WebElement
```

Semantic getters:

```python
driver.get_by_text("Sign in", exact=True)
driver.get_by_role("button", name="Submit")
driver.get_by_label("Email")
driver.get_by_placeholder("Search")
driver.get_by_test_id("login-form")
```

`ElementLocator` scopes a locator under a parent `WebElement`.

---

## `WebElement`

Returned by `find_element*` / locator `element_handle()`.

### Actions

```python
el.click()
el.click_java()          # JS click path
el.clear()
el.focus()
el.send_text("hello")
el.send_key(Keys.ENTER)
el.set_attribute("data-x", "1", is_string=True)
el.remove_attribute("disabled")
```

### Find under element

Same `find_element*` / `query_selector*` family as driver, scoped to the element.

```python
child = el.find_element(By.CSS_SELECTOR, "span.label")
el.query_selector_all("li")
```

### Properties / attributes

| Member | Type | Notes |
|--------|------|-------|
| `text` | property | Visible / text content |
| `value` | property + setter | Form value |
| `inner_html` | property + setter | |
| `outer_html` | property + setter | |
| `height` / `width` | property | |
| `position` | property | Position info |
| `disabled` | property + setter | |
| `is_displayed` | property | |
| `read_only` | property | |
| `get_attribute(name)` | method | |

```python
print(el.text, el.value, el.is_displayed)
el.value = "new"
print(el.get_attribute("class"))
```

---

## Resilient interaction helpers

Retry-oriented helpers (visibility/clickable waits, multiple attempts):

```python
driver.typing_like_human("#email", "user@example.com", press_enter=False)
driver.ensure_typing("#email", "user@example.com", press_enter=True)
driver.ensure_click("button[type=submit]")
driver.ensure_option_select("select#country", "US")      # value / text / index-like
driver.ensure_checked_checkbox("input#terms")
token = driver.get_turnstile_token(
    selector="input[name='cf-turnstile-response']",
    timeout=10,
    poll_frequency=0.25,
)
```

`ensure_*` methods retry (~5 attempts) with short backoff; raise last error / `TimeoutError` on failure.

---

## Waits

### `WebDriverWait`

```python
from dbgidchromium import (
    WebDriverWait,
    presence_of_element_located,
    visibility_of_element_located,
    invisibility_of_element_located,
    element_to_be_clickable,
    By,
)

wait = WebDriverWait(driver, timeout=10, poll_frequency=0.2)
el = wait.until(visibility_of_element_located((By.CSS_SELECTOR, "#ready")))
wait.until_not(presence_of_element_located((By.CSS_SELECTOR, ".spinner")))
```

Expected-condition factories take a locator tuple `(By.*, value)` and return predicates for `until` / `until_not`.

On timeout: `TimeoutError("Time out to wait element")`.

---

## Frames / iframes

```python
frames = driver.detect_iframes(include_elements=False)
# [{id, name, src, same_origin, ...}, ...]

driver.switch_to_iframe(0)                 # index
driver.switch_to_iframe("frame-one")       # name/id
driver.switch_to_iframe(css_element)       # element
driver.switch_to_frame(...)                # alias of switch_to_iframe
driver.switch_to_parent_frame()
driver.switch_to_default_content()

# Selenium-style facade
driver.switch_to.frame(0)
driver.switch_to.iframe("frame-one")
driver.switch_to.parent_frame()
driver.switch_to.default_content()
driver.switch_to.window(handle)
```

### `FrameLocator` (Playwright-style)

```python
fl = driver.frame_locator("#frame-one")
print(fl.info())                 # tag/id/name/src/same_origin/...
print(fl.is_same_origin(), fl.is_cross_origin())

fl.locator("#inside").fill("hi")
fl.get_by_text("hello")
fl.get_by_role("button", name="Go")
nested = fl.frame_locator("#frame-two")

# cross-origin: DOM from parent is blocked; open src top-level if needed
if fl.is_cross_origin():
    fl.open()   # driver.get(iframe src)
```

`NestedFrameLocator` / `FrameScopedLocator` support nested frame trees. Cross-origin frames raise / mark `CrossOriginFrameException` paths — Chromium SOP still applies.

---

## Tabs / pages

```python
handles = driver.window_handles          # list; headless often single-tab
current = driver.current_window_handle
driver.switch_to_window(handles[0])

pages = driver.pages()                   # list[PageHandle]
page = driver.current_page               # property
page = driver.new_page("https://example.com")

page.bring_to_front()
print(page.url, page.title)
page.locator("h1").text_content()
page.frame_locator("iframe")
page.evaluate("() => document.title")
page.screenshot(path="tab.png")
```

---

## `Select` — `<select>` helper

```python
from dbgidchromium import Select

select = Select(driver.find_element(By.CSS_SELECTOR, "select#country"))
print(select.options)
print(select.all_selected_options)
print(select.first_selected_option)

select.select_by_value("us")
select.select_by_index(2)
select.select_by_visible_text("United States")

select.deselect_all()          # multi-select
select.deselect_by_value("us")
select.deselect_by_index(2)
select.deselect_by_visible_text("United States")
```

Non-`<select>` elements → `UnexpectedTagNameException`.

---

## JavaScript

```python
title = driver.execute_script("return document.title")
# *args supported; elements can round-trip depending on marshal path
driver.execute_script("arguments[0].style.border='2px solid red'", el)

driver.override_js_function(long_bootstrap_script)
```

`execute_script` uses a marshaling wrapper + optional DOM attribute side-channel (`EXECUTE_SCRIPT_*_ATTR`) when direct eval is restricted (CSP). Kwargs include `side_channel_polls`, `side_channel_sleep`, `prefer_dom_fallback`.

Returning DOM nodes may yield `WebElement` instances.

---

## Cookies, storage, headers, UA, proxy

```python
driver.set_cookie("session", "abc", url="https://example.com")
print(driver.get_cookie("session", url="https://example.com"))
print(driver.get_cookies(url="https://example.com"))
driver.clear_cookie("session")
driver.clear_cookies()
driver.delete_all_cookie()

driver.set_local_storage("k", "v", is_string=True)
print(driver.get_local_storage())
driver.clear_local_storage()

driver.set_session_storage("k", "v")
print(driver.get_session_storage())
driver.clear_session_storage()

print(driver.user_agent)
driver.user_agent = "MyAgent/1.0"

print(driver.headers)                 # property
driver.headers = {"X-Debug": "1"}     # setter

driver.set_proxy("127.0.0.1", 8080)

driver.clear_browser(clear_cache=True)
```

Load cookies from a `requests`-like cookie jar / list:

```python
from dbgidchromium import load_cookies_from_requests

load_cookies_from_requests(driver, cookies, url="https://example.com")
# or
driver.load_cookies_from_requests(cookies, url="https://example.com")
```

Module helpers: `apply_custom_headers(driver, headers, merge=True)`, `normalize_custom_headers(headers)`, `clear_browser(driver, clear_cache=True)`, `goto(driver, url, **kwargs)`.

---

## IP / ASN spoof headers

Uses bundled `ip2asn-v4-u32.tsv`.

```python
from dbgidchromium import generate_ip, use_spoof, AsnHit

info = generate_ip()
# {"ip": "...", "country": "US", "name": "...", "asn": 1234}

headers = driver.use_spoof(True)          # random public IP + related headers
headers = driver.use_spoof(info)          # explicit
headers = use_spoof(driver, spoof=info, merge=True)

# or via goto
driver.goto("https://httpbin.org/headers", use_spoof=True)
```

Related: `load_ip2asn_u32_tsv`, `lookup_asn`, `random_public_ip_from_db`, `normalize_spoof`, `build_spoof_headers`.

`AsnHit` dataclass fields: `ip`, `asn`, `country`, `name`.

Typical spoof headers include client IP / ASN-style forwarding headers derived from the hit.

---

## Request interceptor (block resource types)

Pass to `goto(..., interceptor=...)` or set via protocol command.

Accepted types (after alias normalize): e.g. `image`, `audio`, `video`, `media`, `font`, `stylesheet`, `script`, `document`, `connect`, …

Aliases examples: `img`→`image`, `css`→`stylesheet`, `xhr`/`fetch`/`ajax`→`connect`, `js`→`script`.

```python
driver.goto(
    "https://example.com",
    interceptor=["image", "font", "media"],  # block these
)
```

Default type set constant: `REQUEST_INTERCEPTOR_DEFAULT_TYPES = ("image", "audio", "video")`.

---

## Network capture (fetch / XHR)

Injects JS hooks; stores records in-page.

```python
driver.network_capture_start(clear=True, max_body_chars=200000)
driver.goto("https://example.com/app", wait_until="javascript")
# ... interact ...
records = driver.network_capture_get(include_document=False)
driver.network_capture_save("capture.json", include_document=False)
driver.network_capture_stop()

# aliases: start_network_capture / stop_network_capture
```

`build_network_capture_script(clear=True, enabled=True, max_body_chars=200000)` returns the raw JS if you need to inject manually.

---

## Screenshots & gestures

```python
png = driver.get_screenshot_as_png(use_rish=None, timeout=20)
driver.save_screenshot("/sdcard/page.png")
driver.get_screenshot_as_file("/sdcard/page.png")
driver.screenshot(path="/sdcard/page.png")       # or as_bytes=True

driver.scroll_to(0, 500)
driver.swipe(100, 800, 100, 200, speed=1)
driver.swipe_up()
driver.swipe_down()
driver.click_java(x=120, y=400)   # coordinate click via Java side
```

`screencap_png(path=None)` captures the **full Android display** (not only WebView) via `screencap`, with rish/file/base64 fallbacks.

---

## reCAPTCHA / Turnstile helpers

```python
token = driver.get_recaptcha_v3_token(action="login")
cf = driver.get_turnstile_token(
    selector="input[name='cf-turnstile-response']",
    timeout=10,
)
```

---

## Async API

Thin asyncio wrappers around sync driver (run blocking calls in a worker).

```python
import asyncio
from dbgidchromium import AsyncWebDriver, AsyncChrome

async def main():
    async with AsyncWebDriver(gui=False, debug=True) as driver:
        await driver.goto("https://example.com", wait_until="javascript")
        loc = driver.locator("h1")
        print(await loc.text_content())
        await driver.screenshot(path="/sdcard/async.png")

asyncio.run(main())
```

Classes:

| Class | Wraps |
|-------|--------|
| `AsyncWebDriver` | `WebDriver` |
| `AsyncWebElement` | `WebElement` |
| `AsyncLocator` | `Locator` |
| `AsyncFrameLocator` | `FrameLocator` |
| `AsyncPageHandle` | `PageHandle` |

`AsyncWebDriver` supports `__aenter__` / `__aexit__`, `goto`, `execute_script`, locator helpers, network capture, screenshots, etc. Unknown attrs proxy to the underlying sync driver as async callables when callable.

---

## Module-level function index

| Function | Summary |
|----------|---------|
| `screencap_png(path=None, use_rish=None, timeout=20)` | Full-display PNG bytes |
| `start_browser_activity(...)` | Start SplashActivity / intent |
| `start_browser_service(...)` | Start MainService headless |
| `foreground_browser(...)` | Foreground UI without wiping tabs |
| `decode_data` / `b64encode` / `b64decode` | Protocol codecs |
| `load_cookies_from_requests(driver, cookies, url="")` | Import cookies |
| `clear_browser(driver, clear_cache=True)` | Clear via driver |
| `generate_ip` / `lookup_asn` / `load_ip2asn_u32_tsv` / … | ASN DB |
| `normalize_spoof` / `build_spoof_headers` / `use_spoof` | Spoof headers |
| `normalize_custom_headers` / `apply_custom_headers` | Header merge/set |
| `build_network_capture_script(...)` | JS capture bootstrap |
| `goto(driver, url, **kwargs)` | Functional `goto` |
| `find_element(driver, locator, command)` | Wait-condition helper |
| `presence_of_element_located` / `visibility_of_element_located` / `invisibility_of_element_located` / `element_to_be_clickable` | Expected conditions |
| `resume_browser_session` / `resume_chrome` / `continue_browser_session` / `continue_chrome` / `detach_browser_session` | Session continuity |

---

## End-to-end examples

### 1) Headless smoke + CSS locators

```python
import dbgidchromium as chromium
from dbgidchromium import By

with chromium.WebDriver(gui=False, debug=True) as driver:
    driver.goto("https://example.com", wait_until="javascript", timeout=30)
    h1 = driver.find_element(By.CSS_SELECTOR, "h1")
    print(h1.text)
    driver.locator("a").first.click()
    print(driver.current_url)
```

### 2) Human-like login form

```python
driver = chromium.WebDriver(gui=True, debug=True)
driver.goto("https://example.com/login", wait_until="javascript")
driver.ensure_typing("#email", "user@example.com")
driver.ensure_typing("#password", "secret", press_enter=False)
driver.ensure_checked_checkbox("#remember")
driver.ensure_click("button[type=submit]")
driver.wait_for_spa_ready(selector="#dashboard", timeout=20)
print(driver.title)
driver.close()
```

### 3) Iframe walk

```python
driver.goto(local_fixture_url, wait_until="javascript")
frames = driver.detect_iframes()
driver.switch_to_iframe(0)
print(driver.find_element(By.CSS_SELECTOR, "#inside").text)
driver.switch_to_default_content()

# or
driver.frame_locator("#frame-one").locator("#inside").text_content()
```

### 4) Network capture + header spoof

```python
with chromium.WebDriver(gui=False) as driver:
    driver.network_capture_start()
    driver.goto(
        "https://httpbin.org/headers",
        use_spoof=True,
        interceptor=["image", "font"],
        wait_until="javascript",
    )
    print(driver.locator("body").inner_text()[:500])
    driver.network_capture_save("out.json")
```

### 5) Wait for element

```python
from dbgidchromium import WebDriverWait, visibility_of_element_located, By

driver.goto("https://example.com")
el = WebDriverWait(driver, 15).until(
    visibility_of_element_located((By.CSS_SELECTOR, "h1"))
)
print(el.text)
```

### 6) Async

```python
import asyncio
from dbgidchromium import AsyncChrome

async def run():
    async with AsyncChrome(gui=False) as page:
        await page.goto("https://example.com", wait_until="complete")
        print(await page.locator("h1").text_content())

asyncio.run(run())
```

### 7) Run bundled example

```bash
python example.py
# prints: dbgidchromium version: 1.1.9
# runs local iframe DOM smoke + google wait_until modes
```

---

## Design notes / limitations

1. **Android-only transport** — needs DBG ID Browser + shell start permissions.
2. **`By.ID` may be unreliable** — use `By.CSS_SELECTOR` with `#id` (browser-side issue on some builds).
3. **Headless vs GUI** — `gui=False` uses service path; default `wait_until` is more aggressive/minimal; `gui_fallback` may open UI if service handshake fails.
4. **Cross-origin iframes** — detectable; DOM access from parent is not possible; use `FrameLocator.open()` to navigate top-level to `src`.
5. **Single-tab headless** — `window_handles` often length 1 in service mode.
6. **Base64 protocol** — all command values encoded; raw socket debugging will look opaque.
7. **Stdlib-first** — core automation does not require Selenium/Playwright packages.

---

## Versioning

Current package version is defined in `__init__.py`:

```python
__version__ = "1.1.9"
```

`setup.py` reads the same field for distribution builds. See `CHANGELOG.md` for history (`1.1.8` documented resilient helpers, release script, README refresh; this tree is **1.1.9**).

```bash
python -c "import dbgidchromium as c; print(c.__version__)"
```

---

## Links

- Package / toolkit: https://github.com/dbgid/dbgidchromium
- Required browser: https://github.com/dbgid/DBG-ID-Browser
- License: MIT (`LICENSE`)

---

## API map (classes)

```
dbgidchromium
├── __version__ = "1.1.9"
├── By, Keys, Command, DictMap, AsnHit
├── Exceptions: WebDriverException, NoSuchElementException, ApplicationClosed,
│               CrossOriginFrameException, InvalidElementStateException,
│               UnexpectedTagNameException
├── RemoteConnection
│   └── WebDriver (= Chrome)
│       ├── WebElement
│       ├── Select
│       ├── WebDriverWait + expected conditions
│       ├── SwitchTo
│       ├── Locator / ElementLocator
│       ├── FrameLocator / NestedFrameLocator / FrameScopedLocator
│       └── PageHandle
└── AsyncWebDriver (= AsyncChrome)
    ├── AsyncWebElement
    ├── AsyncLocator
    ├── AsyncFrameLocator
    └── AsyncPageHandle
```

For behavioral smoke tests mirroring navigation + iframe DOM, see `example.py`.
