"use client"; import { Copy, Layers, Loader2, Package, Terminal } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { LiveAndroidStreamConsole } from "@/components/live-product/LiveAndroidStreamConsole"; type DeviceRow = { serial: string; model?: string; state: string; transport?: string }; type TFn = (zh: string, en: string) => string; type AndroidOverview = { serial?: string; model?: string; brand?: string; android_version?: string; sdk?: string; battery?: string; focus?: string; resolution?: string; rotation?: string; screen_state?: string; }; type PkgRow = { package: string; label?: string }; type UiNode = { label: string; text?: string; content_desc?: string; resource_id?: string; center_x: number; center_y: number; clickable?: boolean; enabled?: boolean; }; const SHELL_SNIPPETS: { label: string; cmd: string }[] = [ { label: "getprop", cmd: "getprop ro.build.version.release" }, { label: "top pkg", cmd: "dumpsys activity activities | grep -E 'mResumedActivity' | tail -n 1" }, { label: "sdcard", cmd: "ls -la /sdcard | head -n 24" }, { label: "net", cmd: "dumpsys connectivity | head -n 40" }, ]; type Props = { t: TFn; notify: (msg: string) => void; fetchJson: (path: string, init?: RequestInit) => Promise; apiBase: string; /** Parent busy (e.g. adb actions); combined with local pending for UI lock. */ busy: string | null; androidSerial: string; setAndroidSerial: (s: string) => void; devices: DeviceRow[]; adbAvailable: boolean; apkPath: string; setApkPath: (v: string) => void; onAndroidAction: (action: string, payload?: Record) => Promise; onCopy: (text: string) => void; onRefreshDevices: () => void; }; export function LiveAndroidWorkstation({ t, notify, fetchJson, apiBase, busy, androidSerial, setAndroidSerial, devices, adbAvailable, apkPath, setApkPath, onAndroidAction, onCopy, onRefreshDevices, }: Props) { const [overview, setOverview] = useState(null); const [overviewErr, setOverviewErr] = useState(null); const [pkgQuery, setPkgQuery] = useState(""); const [pkgList, setPkgList] = useState([]); const [shellCmd, setShellCmd] = useState("getprop ro.product.model"); const [shellOut, setShellOut] = useState(""); const [logcatLines, setLogcatLines] = useState("300"); const [logcatText, setLogcatText] = useState(""); const [logcatErr, setLogcatErr] = useState(null); const [uiNodes, setUiNodes] = useState([]); const [uiErr, setUiErr] = useState(null); const [openUrl, setOpenUrl] = useState("https://"); const [adbInputText, setAdbInputText] = useState(""); const [logcatLive, setLogcatLive] = useState(false); const [pending, setPending] = useState(null); const lock = !!(busy || pending); const loadOverview = useCallback(async () => { if (!androidSerial || !adbAvailable) { setOverview(null); setOverviewErr(null); return; } setPending("overview"); setOverviewErr(null); try { const o = await fetchJson( `/android/overview?serial=${encodeURIComponent(androidSerial)}`, ); setOverview(o); } catch (e) { setOverview(null); setOverviewErr((e as Error).message); } finally { setPending(null); } }, [adbAvailable, androidSerial, fetchJson]); useEffect(() => { void loadOverview(); }, [loadOverview]); const searchPackages = async () => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } setPending("packages"); try { const q = encodeURIComponent(pkgQuery.trim()); const data = await fetchJson<{ packages: PkgRow[] }>( `/android/packages?serial=${encodeURIComponent(androidSerial)}&query=${q}&limit=80`, ); setPkgList(data.packages || []); } catch (e) { notify((e as Error).message); setPkgList([]); } finally { setPending(null); } }; const runShell = async () => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } const cmd = shellCmd.trim(); if (!cmd) return; setPending("shell"); try { const data = await fetchJson<{ output?: string; message?: string }>("/android/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "shell", serial: androidSerial, payload: { command: cmd } }), }); setShellOut(data.output ?? data.message ?? ""); } catch (e) { setShellOut((e as Error).message); } finally { setPending(null); } }; const refreshLogcat = useCallback( async (opts?: { silent?: boolean }) => { if (!androidSerial) { return; } const n = Math.min(3000, Math.max(1, parseInt(logcatLines, 10) || 200)); const silent = !!opts?.silent; if (!silent) setPending("logcat"); setLogcatErr(null); try { const data = await fetchJson<{ text?: string }>( `/android/logcat?serial=${encodeURIComponent(androidSerial)}&lines=${n}`, ); setLogcatText(data.text ?? ""); } catch (e) { setLogcatText(""); setLogcatErr((e as Error).message); } finally { if (!silent) setPending(null); } }, [androidSerial, fetchJson, logcatLines], ); useEffect(() => { if (!logcatLive || !androidSerial || !adbAvailable) return; const id = window.setInterval(() => void refreshLogcat({ silent: true }), 3000); void refreshLogcat({ silent: true }); return () => clearInterval(id); }, [logcatLive, androidSerial, adbAvailable, refreshLogcat]); const loadUiNodes = async () => { if (!androidSerial) { notify(t("请选择设备", "Pick a device")); return; } setPending("ui"); setUiErr(null); try { const data = await fetchJson<{ nodes: UiNode[] }>( `/android/ui?serial=${encodeURIComponent(androidSerial)}&limit=100`, ); setUiNodes(data.nodes || []); } catch (e) { setUiNodes([]); setUiErr((e as Error).message); } finally { setPending(null); } }; const tapNode = async (x: number, y: number) => { await onAndroidAction("tap", { x, y }); }; const sendKey = async (keycode: string) => { await onAndroidAction("key", { keycode }); }; const openUrlGo = async () => { const u = openUrl.trim(); if (!u || u === "https://") { notify(t("填写 URL", "Enter URL")); return; } await onAndroidAction("open_url", { url: u }); }; const sendAdbText = async () => { const raw = adbInputText.trim(); if (!raw) { notify(t("填写要输入的文字", "Enter text")); return; } await onAndroidAction("text", { text: raw }); }; return (

{t("设备与工作台", "Device workstation")}

{!adbAvailable ? ( {t("宿主机未检测到 adb", "adb not found on host")} ) : ( )}
{devices.map((d) => ( ))}
{adbAvailable && devices.length === 0 ? (

{t( "未检测到已授权设备:双头 USB 接板子时请开「USB 调试」并授权;Redroid / 容器安卓可先 `adb connect IP:5555` 再刷新本页。", "No device: enable USB debugging for dual-USB to the board; for Redroid use adb connect IP:5555 then refresh.", )}

) : null}
{adbAvailable && androidSerial ? (

{t("设备信息", "Device info")}

{overviewErr ?

{overviewErr}

: null} {overview ? (
model
{" "}
{overview.brand} {overview.model}
Android
{" "}
{overview.android_version} (sdk {overview.sdk})
display
{" "}
{overview.resolution}
rotation
{" "}
{overview.rotation}
{overview.battery}
{overview.focus}
) : !overviewErr ? (

{t("加载中…", "Loading…")}

) : null}

{t("快捷控制", "Quick controls")}

{t("按键与常用应用;深层调试用下方 Shell / Logcat。", "Keys & apps; use Shell / Logcat for deep debug.")}

{t("滑动手势(中心区域)", "Swipe (center)")}

{(["up", "down", "left", "right"] as const).map((dir) => ( ))}

{t("输入文字(adb input text)", "Type text (adb)")}

{t("仅适合英文与数字;中文需剪贴板或无障碍方案。", "ASCII-focused; CJK needs clipboard.")}

setAdbInputText(e.target.value)} placeholder="hello / 123" onKeyDown={(e) => { if (e.key === "Enter") void sendAdbText(); }} />
setOpenUrl(e.target.value)} placeholder="https://" />
) : null} {adbAvailable && androidSerial ? (

{t("包名检索与启动", "Packages")}

{t("过滤 pm list packages;行内可启动或强停。", "Filter pm list packages; launch or force-stop.")}

setPkgQuery(e.target.value)} placeholder="tiktok / musically / …" onKeyDown={(e) => { if (e.key === "Enter") void searchPackages(); }} />
    {pkgList.map((p) => (
  • {p.package}
  • ))} {!pkgList.length ? (
  • {t("暂无结果,先搜索", "No results")}
  • ) : null}
) : null} {adbAvailable && androidSerial ? (

{t("交互式 Shell", "ADB shell")}

{t("Ctrl+Enter 执行;勿执行不可逆命令。", "Ctrl+Enter to run.")}

{SHELL_SNIPPETS.map((s) => ( ))}