import * as __rspack_external__midscene_shared_img_758d2833 from "@midscene/shared/img";
import * as __rspack_external__midscene_shared_logger_b1dc2426 from "@midscene/shared/logger";
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
import * as __rspack_external_node_module_ab9f2194 from "node:module";
import * as __rspack_external_node_path_c5b9b54f from "node:path";
import { createReportCliCommands, getMidsceneLocationSchema, z } from "@midscene/core";
import { reportCLIError, runToolsCLI } from "@midscene/shared/cli";
import { agentBehaviorInitArgShape, extractAgentBehaviorInitArgs, getAgentInitArgsSignature, shouldRebuildAgentForInitArgs } from "@midscene/shared/agent-tools/agent-behavior-init-args";
import { BaseMidsceneTools } from "@midscene/shared/agent-tools/base-tools";
import { Agent } from "@midscene/core/agent";
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
import node_assert from "node:assert";
import { execFile } from "node:child_process";
import { createDefaultMobileActions, defineAction, resolveTextInputOptions, sendTextSequentially, shouldInputSequentially } from "@midscene/core/device";
import { getTmpFile, sleep } from "@midscene/core/utils";
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, MIDSCENE_ANDROID_SCREENSHOT_STRATEGY, globalConfigManager } from "@midscene/shared/env";
import { ADB, getSdkRootFromEnv } from "appium-adb";
var __webpack_modules__ = {
    "./src/resource-path.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.d(__webpack_exports__, {
            J: ()=>resolveExternalResourcePath
        });
        var node_fs__rspack_import_0 = __webpack_require__("node:fs");
        function resolveExternalResourcePath(resourcePath, pathExists = node_fs__rspack_import_0.existsSync) {
            const unpackedPath = resourcePath.replace(/\.asar([/\\])/, '.asar.unpacked$1');
            return unpackedPath !== resourcePath && pathExists(unpackedPath) ? unpackedPath : resourcePath;
        }
    },
    "./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
        __webpack_require__.d(__webpack_exports__, {
            GJ: ()=>isScrcpyFreshFrameUnavailableError,
            ScrcpyScreenshotManager: ()=>ScrcpyScreenshotManager,
            ov: ()=>DEFAULT_SCRCPY_CONFIG
        });
        var node_module__rspack_import_0 = __webpack_require__("node:module");
        var node_path__rspack_import_1 = __webpack_require__("node:path");
        var _midscene_shared_img__rspack_import_2 = __webpack_require__("@midscene/shared/img");
        var _midscene_shared_logger__rspack_import_3 = __webpack_require__("@midscene/shared/logger");
        var _resource_path__rspack_import_4 = __webpack_require__("./src/resource-path.ts");
        function _define_property(obj, key, value) {
            if (key in obj) Object.defineProperty(obj, key, {
                value: value,
                enumerable: true,
                configurable: true,
                writable: true
            });
            else obj[key] = value;
            return obj;
        }
        const debugScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy');
        const warnScrcpy = (0, _midscene_shared_logger__rspack_import_3.getDebug)('android:scrcpy', {
            console: true
        });
        const NAL_TYPE_IDR = 5;
        const NAL_TYPE_SPS = 7;
        const NAL_TYPE_PPS = 8;
        const NAL_TYPE_MASK = 0x1f;
        const DEFAULT_MAX_SIZE = 0;
        const DEFAULT_VIDEO_BIT_RATE = 100000000;
        const MAX_VIDEO_BIT_RATE = 100000000;
        const DEFAULT_IDLE_TIMEOUT_MS = 30000;
        const MAX_KEYFRAME_WAIT_MS = 5000;
        const FRESH_FRAME_TIMEOUT_MS = 300;
        const VIDEO_RESET_DELAY_MS = 10;
        const DEFAULT_VIDEO_RESET_FRAME_TIMEOUT_MS = 800;
        const KEYFRAME_POLL_INTERVAL_MS = 200;
        const MAX_SCAN_BYTES = 1000;
        const MAX_SERVER_OUTPUT_LINES = 100;
        const SERVER_OUTPUT_DRAIN_TIMEOUT_MS = 500;
        const MAX_FRAME_AGE_US = 500000n;
        const FRAME_FRESHNESS_WARN_INTERVAL_MS = 5000;
        const DEVICE_UPTIME_COMMAND = [
            'dumpsys',
            'power'
        ];
        const BUSY_LOOP_WINDOW_MS = 1000;
        const BUSY_LOOP_MAX_READS = 500;
        const BUSY_LOOP_COOLDOWN_MS = 50;
        const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
        const DEFAULT_SCRCPY_CONFIG = {
            enabled: false,
            maxSize: DEFAULT_MAX_SIZE,
            idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
            videoBitRate: DEFAULT_VIDEO_BIT_RATE,
            videoResetFrameTimeoutMs: DEFAULT_VIDEO_RESET_FRAME_TIMEOUT_MS
        };
        const SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE = 'ERR_SCRCPY_FRESH_FRAME_UNAVAILABLE';
        class ScrcpyFreshFrameUnavailableError extends Error {
            constructor(message, options){
                super(message, options), _define_property(this, "code", SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE), _define_property(this, "failureKind", void 0), _define_property(this, "timeoutMs", void 0), _define_property(this, "videoBitRate", void 0);
                this.name = 'ScrcpyFreshFrameUnavailableError';
                this.failureKind = options.failureKind;
                this.timeoutMs = options.timeoutMs;
                this.videoBitRate = options.videoBitRate;
            }
        }
        class ScrcpyFrameWaitTimeoutError extends Error {
            constructor(message, timeoutMs){
                super(message), _define_property(this, "timeoutMs", void 0), this.timeoutMs = timeoutMs;
                this.name = 'ScrcpyFrameWaitTimeoutError';
            }
        }
        function isScrcpyFreshFrameUnavailableError(error) {
            return 'object' == typeof error && null !== error && 'code' in error && error.code === SCRCPY_FRESH_FRAME_UNAVAILABLE_ERROR_CODE;
        }
        function parseDeviceUptimeMs(output) {
            const match = output.match(/mLastWakeTime=(\d+)\s+\((?:(\d+) ms ago|in (\d+) ms|now)\)/);
            if (!match) throw new Error(`Unable to read Android device uptime from dumpsys power: ${output.trim() || '<empty output>'}`);
            const referenceMs = BigInt(match[1]);
            if (match[2]) return referenceMs + BigInt(match[2]);
            if (match[3]) return referenceMs - BigInt(match[3]);
            return referenceMs;
        }
        function isKeyFrameNalType(nalUnitType) {
            return nalUnitType === NAL_TYPE_IDR || nalUnitType === NAL_TYPE_SPS || nalUnitType === NAL_TYPE_PPS;
        }
        function detectH264KeyFrame(buffer) {
            const scanLimit = Math.min(buffer.length - 4, MAX_SCAN_BYTES);
            for(let i = 0; i < scanLimit; i++)if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x00 === buffer[i + 2] && 0x01 === buffer[i + 3]) {
                const nalUnitType = buffer[i + 4] & NAL_TYPE_MASK;
                if (isKeyFrameNalType(nalUnitType)) return true;
            } else if (0x00 === buffer[i] && 0x00 === buffer[i + 1] && 0x01 === buffer[i + 2]) {
                const nalUnitType = buffer[i + 3] & NAL_TYPE_MASK;
                if (isKeyFrameNalType(nalUnitType)) return true;
            }
            return false;
        }
        class ScrcpyScreenshotManager {
            async validateEnvironment() {
                await this.ensureFfmpegAvailable();
            }
            async ensureConnected() {
                if (this.disposed) throw new Error('Scrcpy manager has been disposed');
                if (this.scrcpyClient && this.videoStream) {
                    debugScrcpy('Scrcpy already connected');
                    await this.ensureFrameClockCalibration();
                    this.resetIdleTimer();
                    return;
                }
                if (this.connectionPromise) {
                    debugScrcpy('Connection already in progress, sharing the same attempt...');
                    await this.connectionPromise;
                    return;
                }
                const connectionPromise = this.connectScrcpy();
                this.connectionPromise = connectionPromise;
                try {
                    await connectionPromise;
                } finally{
                    if (this.connectionPromise === connectionPromise) this.connectionPromise = null;
                }
            }
            async connectScrcpy() {
                const serverOutput = [];
                let serverOutputTask = null;
                try {
                    debugScrcpy('Starting scrcpy connection...');
                    const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
                    const { DefaultServerPath } = await import("@yume-chan/scrcpy");
                    const serverBinPath = this.resolveServerBinPath();
                    await this.pushServer(serverBinPath, DefaultServerPath);
                    const scrcpyOptions = await this.createScrcpyOptions();
                    await this.ensureFrameClockCalibration();
                    this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
                    serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
                    const videoStreamPromise = this.scrcpyClient.videoStream;
                    if (!videoStreamPromise) throw new Error('Scrcpy client did not provide video stream');
                    this.videoStream = await videoStreamPromise;
                    const { width = 0, height = 0 } = this.videoStream.metadata;
                    debugScrcpy(`Video stream started: ${width}x${height}`);
                    this.videoResolution = {
                        width,
                        height
                    };
                    this.advanceStreamEpoch();
                    this.streamReaderEpoch++;
                    this.streamStartupWindow = {
                        deadlineAt: Date.now() + MAX_KEYFRAME_WAIT_MS
                    };
                    this.startFrameConsumer();
                    this.resetIdleTimer();
                    this.isInitialized = true;
                    debugScrcpy('Scrcpy connection established');
                } catch (error) {
                    debugScrcpy(`Failed to connect scrcpy: ${error}`);
                    await this.disconnect();
                    if (serverOutputTask) await Promise.race([
                        serverOutputTask,
                        new Promise((resolve)=>setTimeout(resolve, SERVER_OUTPUT_DRAIN_TIMEOUT_MS))
                    ]);
                    throw this.createConnectionError(error, serverOutput);
                }
            }
            async createScrcpyOptions() {
                const [{ AdbScrcpyOptions3_3_3 }, { ScrcpyInstanceId }] = await Promise.all([
                    import("@yume-chan/adb-scrcpy"),
                    import("@yume-chan/scrcpy")
                ]);
                return new AdbScrcpyOptions3_3_3({
                    audio: false,
                    control: true,
                    powerOn: false,
                    clipboardAutosync: false,
                    tunnelForward: true,
                    scid: ScrcpyInstanceId.random(),
                    maxSize: this.options.maxSize,
                    videoBitRate: this.options.videoBitRate,
                    maxFps: 10,
                    sendFrameMeta: true,
                    videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2,repeat-previous-frame-after=0'
                });
            }
            async collectServerOutput(output, lines) {
                const reader = output.getReader();
                try {
                    while(true){
                        const { done, value } = await reader.read();
                        if (done) break;
                        const outputLine = value.trimEnd();
                        if (outputLine) debugScrcpy(`Scrcpy server: ${outputLine}`);
                        lines.push(value);
                        if (lines.length > MAX_SERVER_OUTPUT_LINES) lines.splice(0, lines.length - MAX_SERVER_OUTPUT_LINES);
                    }
                } catch (error) {
                    debugScrcpy(`Failed to read scrcpy server output: ${error}`);
                } finally{
                    reader.releaseLock();
                }
            }
            createConnectionError(error, serverOutput) {
                const errorOutput = this.getErrorOutput(error);
                const output = [
                    ...new Set([
                        ...errorOutput,
                        ...serverOutput
                    ])
                ].filter((line)=>line.trim().length > 0);
                const message = error instanceof Error ? error.message : String(error);
                const outputDetails = output.length > 0 ? `\nScrcpy server output:\n${output.join('\n')}` : '';
                return new Error(`Failed to connect scrcpy: ${message}${outputDetails}`, {
                    cause: error
                });
            }
            getErrorOutput(error) {
                if ('object' != typeof error || null === error || !('output' in error)) return [];
                const output = error.output;
                if (!Array.isArray(output)) return [];
                return output.filter((line)=>'string' == typeof line);
            }
            resolveServerBinPath() {
                const androidPkgJson = (0, node_module__rspack_import_0.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
                return (0, _resource_path__rspack_import_4.J)(node_path__rspack_import_1["default"].join(node_path__rspack_import_1["default"].dirname(androidPkgJson), 'bin', 'scrcpy-server'));
            }
            getFfmpegPath() {
                try {
                    const dynamicRequire = (0, node_module__rspack_import_0.createRequire)(import.meta.url);
                    const ffmpegInstaller = dynamicRequire('@ffmpeg-installer/ffmpeg');
                    debugScrcpy(`Using ffmpeg from npm package: ${ffmpegInstaller.path}`);
                    return ffmpegInstaller.path;
                } catch (error) {
                    debugScrcpy('Using system ffmpeg (npm package not found)');
                    return 'ffmpeg';
                }
            }
            startFrameConsumer() {
                if (!this.videoStream) return;
                const reader = this.videoStream.stream.getReader();
                this.streamReader = reader;
                this.consumeFramesLoop(reader, this.streamReaderEpoch);
            }
            async consumeFramesLoop(reader, streamReaderEpoch = this.streamReaderEpoch) {
                let readCount = 0;
                let windowStart = Date.now();
                let lastBusyWarn = 0;
                let totalReads = 0;
                let endReason = 'stream closed';
                try {
                    while(true){
                        const { done, value } = await reader.read();
                        if (done) break;
                        totalReads++;
                        readCount++;
                        const now = Date.now();
                        const elapsed = now - windowStart;
                        if (elapsed >= BUSY_LOOP_WINDOW_MS) {
                            const readsPerSec = readCount / elapsed * 1000;
                            if (readCount > BUSY_LOOP_MAX_READS) {
                                if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
                                    warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
                                    lastBusyWarn = now;
                                }
                                await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
                            } else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
                            readCount = 0;
                            windowStart = Date.now();
                        }
                        this.processFrame(value, streamReaderEpoch);
                    }
                } catch (error) {
                    endReason = 'stream error';
                    debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
                }
                if (this.streamReader === reader) await this.disconnect();
                debugScrcpy(`Frame consumer loop ended (${endReason}, total reads: ${totalReads})`);
            }
            processFrame(packet, streamReaderEpoch = this.streamReaderEpoch) {
                if (streamReaderEpoch !== this.streamReaderEpoch) return;
                if ('configuration' === packet.type) {
                    this.spsHeader = Buffer.from(packet.data);
                    debugScrcpy(`Received SPS/PPS configuration: ${this.spsHeader.length}B`);
                    return;
                }
                const activeReset = this.videoResetState;
                if (activeReset) activeReset.dataPacketsSeen++;
                const receivedAtUs = this.monotonicTimeUs();
                if (!this.isFrameFresh(packet.pts)) return;
                const frameBuffer = Buffer.from(packet.data);
                const isKeyFrame = detectH264KeyFrame(frameBuffer);
                if (isKeyFrame && this.spsHeader) {
                    const timing = this.estimateFrameTiming(packet.pts, receivedAtUs);
                    this.lastRawKeyframe = frameBuffer;
                    this.lastRawKeyframeAt = timing.capturedAt;
                    this.lastRawKeyframePtsUs = packet.pts;
                    this.lastRawKeyframeEstimatedAgeMs = timing.estimatedAgeMs;
                    this.hasEstablishedVideoFrame = true;
                    this.streamStartupWindow = null;
                    const frame = {
                        data: frameBuffer,
                        header: this.spsHeader,
                        ptsUs: packet.pts,
                        estimatedAgeMs: timing.estimatedAgeMs,
                        streamEpoch: this.streamEpoch,
                        capturedAt: this.lastRawKeyframeAt
                    };
                    this.lastRawKeyframeStreamEpoch = this.streamEpoch;
                    const resetState = this.videoResetState;
                    if (resetState) {
                        resetState.frameAccepted = true;
                        if (null === resetState.writePromise) this.videoResetState = null;
                    }
                    if (this.keyframeResolvers.length > 0) this.notifyKeyframeWaiters(frame);
                    if (this.keyframeListeners.size > 0 && this.isFrameAgeAcceptable(packet.pts, receivedAtUs)) {
                        for (const listener of this.keyframeListeners)try {
                            listener(frame);
                        } catch (error) {
                            debugScrcpy(`keyframe listener error: ${error}`);
                        }
                        this.resetIdleTimer();
                    }
                }
            }
            async readDeviceClockCalibration() {
                const startedAtUs = this.monotonicTimeUs();
                const startedAtWallMs = Date.now();
                const shellProtocol = this.adb.subprocess.shellProtocol;
                let output;
                if (shellProtocol) {
                    const result = await shellProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
                    if (0 !== result.exitCode) throw new Error(`Unable to read Android device uptime (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
                    output = result.stdout;
                } else output = await this.adb.subprocess.noneProtocol.spawnWaitText(DEVICE_UPTIME_COMMAND);
                const finishedAtUs = this.monotonicTimeUs();
                const finishedAtWallMs = Date.now();
                return {
                    deviceUptimeUs: 1000n * parseDeviceUptimeMs(output),
                    hostMonotonicUs: startedAtUs + (finishedAtUs - startedAtUs) / 2n,
                    hostWallTimeMs: startedAtWallMs + (finishedAtWallMs - startedAtWallMs) / 2,
                    roundTripUs: finishedAtUs - startedAtUs
                };
            }
            async sampleFrameClockCalibration(force) {
                if (!force && this.deviceClockCalibration) return;
                if (this.deviceClockCalibrationPromise) {
                    await this.deviceClockCalibrationPromise;
                    if (!this.deviceClockCalibration) throw new Error('Scrcpy stream epoch changed while calibrating the frame clock');
                    return;
                }
                const calibrationPromise = this.readDeviceClockCalibration();
                this.deviceClockCalibrationPromise = calibrationPromise;
                try {
                    const sampledCalibration = await calibrationPromise;
                    if (this.deviceClockCalibrationPromise !== calibrationPromise) throw new Error('Scrcpy stream epoch changed while calibrating the frame clock');
                    const previousCalibration = this.deviceClockCalibration;
                    const calibration = force && previousCalibration && previousCalibration.roundTripUs <= sampledCalibration.roundTripUs ? previousCalibration : sampledCalibration;
                    this.deviceClockCalibration = calibration;
                    if (!previousCalibration) {
                        this.frameFreshnessBarrierPending = false;
                        this.lastFramePtsUs = null;
                        this.frameFreshnessError = null;
                    }
                    debugScrcpy(force ? `Recalibrated scrcpy frame clock for stream epoch (sample RTT=${Number(sampledCalibration.roundTripUs) / 1000}ms, selected RTT=${Number(calibration.roundTripUs) / 1000}ms)` : `Calibrated scrcpy frame clock for stream epoch (RTT=${Number(calibration.roundTripUs) / 1000}ms)`);
                } finally{
                    if (this.deviceClockCalibrationPromise === calibrationPromise) this.deviceClockCalibrationPromise = null;
                }
            }
            async ensureFrameClockCalibration() {
                await this.sampleFrameClockCalibration(false);
            }
            async recalibrateFrameClock() {
                await this.sampleFrameClockCalibration(true);
            }
            async setFreshnessBarrier(reason, options = {}) {
                const cachedFrame = this.getCachedKeyframeCandidate();
                const generation = ++this.frameFreshnessBarrierGeneration;
                this.frameFreshnessBarrierPending = true;
                this.clearFrameCache();
                try {
                    const calibration = this.deviceClockCalibration;
                    if (!calibration) throw new Error('Scrcpy frame clock is not calibrated for the current stream epoch');
                    const hostMonotonicUs = options.hostMonotonicUs ?? this.monotonicTimeUs();
                    const estimatedDeviceNowUs = this.estimateDeviceTimeUs(hostMonotonicUs, calibration);
                    const conservativeDeviceNowUs = estimatedDeviceNowUs + this.getCalibrationUncertaintyUs(calibration);
                    const barrierPtsUs = (conservativeDeviceNowUs / 1000n + 1n) * 1000n;
                    if (generation !== this.frameFreshnessBarrierGeneration) return this.frameFreshnessBarrierPtsUs ?? barrierPtsUs;
                    this.frameFreshnessBarrierPtsUs = null === this.frameFreshnessBarrierPtsUs || barrierPtsUs > this.frameFreshnessBarrierPtsUs ? barrierPtsUs : this.frameFreshnessBarrierPtsUs;
                    this.frameFreshnessBarrierReason = reason;
                    this.frameFreshnessBarrierPending = false;
                    this.frameFreshnessError = null;
                    this.lastFramePtsUs = null;
                    if (cachedFrame?.ptsUs !== void 0 && cachedFrame.ptsUs >= this.frameFreshnessBarrierPtsUs) {
                        this.restoreFrameCache(cachedFrame);
                        debugScrcpy(`Preserved cached frame PTS ${cachedFrame.ptsUs}µs because it crosses the newly armed ${reason} barrier`);
                    } else this.clearFrameCache();
                    debugScrcpy(`Armed frame freshness barrier at PTS ${this.frameFreshnessBarrierPtsUs}µs (${reason}, projected from stream-epoch clock anchor, uncertainty<=${Number(this.getCalibrationUncertaintyUs(calibration)) / 1000}ms)`);
                    return this.frameFreshnessBarrierPtsUs;
                } catch (error) {
                    if (generation === this.frameFreshnessBarrierGeneration) {
                        this.frameFreshnessBarrierPending = false;
                        this.frameFreshnessError = new Error(`Unable to establish scrcpy frame freshness barrier (${reason}): ${error instanceof Error ? error.message : String(error)}`, {
                            cause: error
                        });
                    }
                    throw this.frameFreshnessError ?? error;
                }
            }
            isFrameFresh(packetPtsUs) {
                if (this.frameFreshnessBarrierPending) return false;
                if (void 0 !== packetPtsUs) {
                    if (null !== this.lastFramePtsUs && packetPtsUs < this.lastFramePtsUs) {
                        this.frameFreshnessError = new Error('Scrcpy frame PTS moved backwards; refusing frames until the device clock anchor is recalibrated');
                        this.frameFreshnessBarrierPending = true;
                        this.frameFreshnessBarrierPtsUs = null;
                        this.frameFreshnessBarrierReason = null;
                        this.deviceClockCalibration = null;
                        this.hasRetriedUncertainClockCalibration = false;
                        this.clearFrameCache();
                        this.warnFrameFreshness();
                        return false;
                    }
                    this.lastFramePtsUs = packetPtsUs;
                }
                if (null === this.frameFreshnessBarrierPtsUs) return true;
                if (void 0 === packetPtsUs) {
                    this.frameFreshnessError = new Error('Scrcpy frame has no PTS metadata; cannot prove that it is newer than the freshness barrier');
                    this.warnFrameFreshness();
                    return false;
                }
                if (packetPtsUs >= this.frameFreshnessBarrierPtsUs) {
                    if (this.frameFreshnessError) debugScrcpy(`Scrcpy video crossed the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier at PTS ${packetPtsUs}µs`);
                    this.frameFreshnessError = null;
                    return true;
                }
                const behindBarrierUs = this.frameFreshnessBarrierPtsUs - packetPtsUs;
                this.frameFreshnessError = new Error(`Scrcpy frame predates the ${this.frameFreshnessBarrierReason ?? 'active'} freshness barrier by ${Number(behindBarrierUs) / 1000}ms; refusing to use it`);
                this.clearFrameCache();
                debugScrcpy(this.frameFreshnessError.message);
                return false;
            }
            estimateFrameAgeUs(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
                return this.estimateFrameAge(packetPtsUs, hostMonotonicUs)?.estimatedAgeUs ?? null;
            }
            estimateFrameAge(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
                const calibration = this.deviceClockCalibration;
                if (void 0 === packetPtsUs || !calibration) return null;
                const estimatedDeviceNowUs = this.estimateDeviceTimeUs(hostMonotonicUs, calibration);
                const estimatedAgeUs = estimatedDeviceNowUs > packetPtsUs ? estimatedDeviceNowUs - packetPtsUs : 0n;
                const calibrationUncertaintyUs = this.getCalibrationUncertaintyUs(calibration);
                return {
                    estimatedAgeUs,
                    calibrationUncertaintyUs,
                    upperBoundUs: estimatedAgeUs + calibrationUncertaintyUs
                };
            }
            estimateDeviceTimeUs(hostMonotonicUs, calibration) {
                return calibration.deviceUptimeUs + (hostMonotonicUs - calibration.hostMonotonicUs);
            }
            getCalibrationUncertaintyUs(calibration) {
                return (calibration.roundTripUs + 1n) / 2n;
            }
            isFrameAgeAcceptable(packetPtsUs, hostMonotonicUs = this.monotonicTimeUs()) {
                const age = this.estimateFrameAge(packetPtsUs, hostMonotonicUs);
                if (null === age) {
                    this.frameFreshnessError = new Error(void 0 === packetPtsUs ? 'Scrcpy frame has no PTS metadata; cannot prove its absolute age' : 'Scrcpy frame clock is not calibrated; cannot prove its absolute age');
                    this.warnFrameFreshness();
                    return false;
                }
                if (age.upperBoundUs <= MAX_FRAME_AGE_US) {
                    if (this.frameFreshnessError) {
                        debugScrcpy(`Scrcpy frame age recovered (upper bound=${Number(age.upperBoundUs) / 1000}ms, estimated=${Number(age.estimatedAgeUs) / 1000}ms, clock uncertainty<=${Number(age.calibrationUncertaintyUs) / 1000}ms)`);
                        this.frameFreshnessError = null;
                    }
                    return true;
                }
                this.frameFreshnessError = new Error(`Scrcpy frame absolute age upper bound is ${Number(age.upperBoundUs) / 1000}ms (estimated=${Number(age.estimatedAgeUs) / 1000}ms, clock uncertainty<=${Number(age.calibrationUncertaintyUs) / 1000}ms), exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit`);
                this.warnFrameFreshness();
                return false;
            }
            warnFrameFreshness() {
                if (!this.frameFreshnessError) return;
                const now = Date.now();
                if (now - this.lastFrameFreshnessWarningAt >= FRAME_FRESHNESS_WARN_INTERVAL_MS) {
                    warnScrcpy(this.frameFreshnessError.message);
                    this.lastFrameFreshnessWarningAt = now;
                }
            }
            estimateFrameTiming(packetPtsUs, receivedAtUs) {
                const calibration = this.deviceClockCalibration;
                if (void 0 === packetPtsUs || !calibration) return {
                    capturedAt: Date.now()
                };
                const estimatedAgeUs = this.estimateFrameAgeUs(packetPtsUs, receivedAtUs) ?? 0n;
                const estimatedAgeMs = Number(estimatedAgeUs) / 1000;
                const receivedAtWallTimeMs = calibration.hostWallTimeMs + Number(receivedAtUs - calibration.hostMonotonicUs) / 1000;
                return {
                    capturedAt: receivedAtWallTimeMs - estimatedAgeMs,
                    estimatedAgeMs
                };
            }
            clearFrameCache() {
                this.lastRawKeyframe = null;
                this.lastRawKeyframeAt = 0;
                this.lastRawKeyframePtsUs = void 0;
                this.lastRawKeyframeEstimatedAgeMs = void 0;
                this.lastRawKeyframeStreamEpoch = void 0;
            }
            restoreFrameCache(frame) {
                this.lastRawKeyframe = frame.data;
                this.lastRawKeyframeAt = frame.capturedAt;
                this.lastRawKeyframePtsUs = frame.ptsUs;
                this.lastRawKeyframeEstimatedAgeMs = frame.estimatedAgeMs;
                this.lastRawKeyframeStreamEpoch = frame.streamEpoch;
            }
            monotonicTimeUs() {
                return process.hrtime.bigint() / 1000n;
            }
            advanceStreamEpoch() {
                this.streamEpoch = Symbol('scrcpy-stream');
            }
            resetFrameFreshnessState() {
                this.frameFreshnessBarrierPtsUs = null;
                this.frameFreshnessBarrierReason = null;
                this.streamStartupWindow = null;
                this.frameFreshnessBarrierPending = false;
                this.frameFreshnessBarrierGeneration = 0;
                this.deviceClockCalibration = null;
                this.deviceClockCalibrationPromise = null;
                this.hasRetriedUncertainClockCalibration = false;
                this.lastFramePtsUs = null;
                this.frameFreshnessError = null;
                this.lastFrameFreshnessWarningAt = 0;
                this.hasEstablishedVideoFrame = false;
                this.videoResetState = null;
            }
            async requestVideoReset(deadlineAt = Date.now() + FRESH_FRAME_TIMEOUT_MS) {
                const activeReset = this.videoResetState;
                if (activeReset) {
                    if (activeReset.writePromise) await this.waitForVideoResetWrite(activeReset.writePromise, deadlineAt);
                    return;
                }
                const controller = this.scrcpyClient?.controller;
                if (!controller?.resetVideo) throw new Error('Scrcpy control channel is unavailable for video reset');
                const resetState = {
                    previousSpsHeader: this.spsHeader,
                    writePromise: null,
                    frameAccepted: false,
                    dataPacketsSeen: 0
                };
                this.videoResetState = resetState;
                this.advanceStreamEpoch();
                this.clearFrameCache();
                this.spsHeader = null;
                const resetPromise = Promise.resolve().then(()=>controller.resetVideo()).then(()=>{
                    debugScrcpy('Requested scrcpy in-band video reset');
                    if (this.videoResetState === resetState) {
                        resetState.writePromise = null;
                        if (resetState.frameAccepted) this.videoResetState = null;
                    }
                }, (error)=>{
                    if (this.videoResetState === resetState) {
                        if (null === this.spsHeader) this.spsHeader = resetState.previousSpsHeader;
                        this.videoResetState = null;
                    }
                    throw error;
                });
                resetState.writePromise = resetPromise;
                resetPromise.catch(()=>{});
                await this.waitForVideoResetWrite(resetPromise, deadlineAt);
            }
            async waitForVideoResetWrite(writePromise, deadlineAt) {
                const remainingMs = deadlineAt - Date.now();
                if (remainingMs <= 0) throw new Error('Scrcpy video reset exceeded the freshness deadline');
                let timeout;
                try {
                    await Promise.race([
                        writePromise,
                        new Promise((_resolve, reject)=>{
                            timeout = setTimeout(()=>reject(new Error('Scrcpy video reset exceeded the freshness deadline')), remainingMs);
                        })
                    ]);
                } finally{
                    if (timeout) clearTimeout(timeout);
                }
            }
            subscribeKeyframes(listener) {
                this.keyframeListeners.add(listener);
                this.resetIdleTimer();
                return ()=>{
                    this.keyframeListeners.delete(listener);
                    this.resetIdleTimer();
                };
            }
            getLatestRawKeyframe() {
                const frame = this.getCachedKeyframeCandidate();
                if (!frame || !this.isFrameAgeAcceptable(frame.ptsUs)) return null;
                return frame;
            }
            getCachedKeyframeCandidate() {
                if (!this.lastRawKeyframe || !this.spsHeader) return null;
                return {
                    data: this.lastRawKeyframe,
                    header: this.spsHeader,
                    ptsUs: this.lastRawKeyframePtsUs,
                    estimatedAgeMs: this.lastRawKeyframeEstimatedAgeMs,
                    streamEpoch: this.lastRawKeyframeStreamEpoch,
                    capturedAt: this.lastRawKeyframeAt
                };
            }
            async decodeRawKeyframeToJpeg(frame) {
                const result = await this.decodeH264ToJpeg(Buffer.concat([
                    frame.header,
                    frame.data
                ]));
                this.recordDecodedFrame(frame, result);
                return result;
            }
            recordDecodedFrame(frame, jpeg) {
                if (frame.streamEpoch !== this.streamEpoch) return;
                const decodedResolution = (0, _midscene_shared_img__rspack_import_2.encodedImageInfoOfBuffer)(jpeg);
                if (this.videoResolution?.width !== decodedResolution.width || this.videoResolution?.height !== decodedResolution.height) {
                    debugScrcpy(`Updating scrcpy resolution from ${this.videoResolution?.width ?? 0}x${this.videoResolution?.height ?? 0} to decoded frame ${decodedResolution.width}x${decodedResolution.height}`);
                    this.videoResolution = decodedResolution;
                }
                this.hasEstablishedVideoFrame = true;
                this.streamStartupWindow = null;
            }
            async isPlanningCandidateFreshEnough(candidate) {
                if (void 0 === candidate.ptsUs) throw new Error('Scrcpy frame has no PTS metadata; cannot prove planning freshness');
                const ageEvaluatedAtUs = this.monotonicTimeUs();
                let age = this.estimateFrameAge(candidate.ptsUs, ageEvaluatedAtUs);
                if (null === age) throw new Error('Scrcpy frame clock is not calibrated; cannot prove planning freshness');
                if (age.upperBoundUs <= MAX_FRAME_AGE_US) return true;
                const uncertaintyOnlyExceededLimit = age.estimatedAgeUs <= MAX_FRAME_AGE_US && age.upperBoundUs > MAX_FRAME_AGE_US;
                if (uncertaintyOnlyExceededLimit) {
                    if (!this.hasRetriedUncertainClockCalibration) {
                        this.hasRetriedUncertainClockCalibration = true;
                        debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs exceeded the ${Number(MAX_FRAME_AGE_US / 1000n)}ms upper-bound limit only because clock uncertainty was ${Number(age.calibrationUncertaintyUs) / 1000}ms; recalibrating once before deciding freshness`);
                        try {
                            await this.recalibrateFrameClock();
                        } catch (error) {
                            debugScrcpy(`Unable to recalibrate scrcpy frame clock after an uncertainty-only freshness result: ${error}`);
                        }
                        if (void 0 !== candidate.streamEpoch && candidate.streamEpoch !== this.streamEpoch) throw new Error('Scrcpy stream epoch changed while recalibrating planning frame freshness');
                        const recalibratedAge = this.estimateFrameAge(candidate.ptsUs, ageEvaluatedAtUs);
                        if (recalibratedAge) age = recalibratedAge;
                        if (age.upperBoundUs <= MAX_FRAME_AGE_US) {
                            debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs accepted after clock recalibration (estimated=${Number(age.estimatedAgeUs) / 1000}ms, uncertainty<=${Number(age.calibrationUncertaintyUs) / 1000}ms)`);
                            return true;
                        }
                    }
                    if (age.estimatedAgeUs <= MAX_FRAME_AGE_US) {
                        debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs remains uncertainty-only after the calibration retry; accepting estimated age ${Number(age.estimatedAgeUs) / 1000}ms despite clock uncertainty<=${Number(age.calibrationUncertaintyUs) / 1000}ms`);
                        return true;
                    }
                }
                debugScrcpy(`Planning candidate PTS ${candidate.ptsUs}µs has absolute age upper bound ${Number(age.upperBoundUs) / 1000}ms (estimated=${Number(age.estimatedAgeUs) / 1000}ms, clock uncertainty<=${Number(age.calibrationUncertaintyUs) / 1000}ms), exceeding the ${Number(MAX_FRAME_AGE_US / 1000n)}ms limit; arming a planning freshness barrier`);
                return false;
            }
            async waitForPlanningFrame() {
                let planningBarrierArmed = false;
                let videoResetAttempted = false;
                let videoResetWritten = false;
                const planningStartedAt = Date.now();
                const startupWindowRemainingMs = this.streamStartupWindow ? this.streamStartupWindow.deadlineAt - planningStartedAt : 0;
                if (this.streamStartupWindow && startupWindowRemainingMs <= 0) this.streamStartupWindow = null;
                let deadline = planningStartedAt + (startupWindowRemainingMs > 0 ? startupWindowRemainingMs : FRESH_FRAME_TIMEOUT_MS);
                let candidate = this.getCachedKeyframeCandidate();
                while(true){
                    if (candidate) {
                        if (await this.isPlanningCandidateFreshEnough(candidate)) return candidate;
                        if (planningBarrierArmed) this.clearFrameCache();
                        else {
                            await this.setFreshnessBarrier('stale planning frame');
                            planningBarrierArmed = true;
                            deadline = Date.now() + FRESH_FRAME_TIMEOUT_MS;
                        }
                    }
                    const remainingMs = deadline - Date.now();
                    if (remainingMs <= 0) {
                        if (videoResetWritten) throw this.createVideoResetFrameTimeoutError();
                        throw new Error(`No scrcpy frame crossed the active freshness target within ${FRESH_FRAME_TIMEOUT_MS}ms`);
                    }
                    const shouldTryVideoReset = !videoResetAttempted && this.hasEstablishedVideoFrame && null === this.streamStartupWindow && Boolean(this.scrcpyClient?.controller?.resetVideo) && remainingMs > VIDEO_RESET_DELAY_MS;
                    const nextFrameWaitMs = shouldTryVideoReset ? VIDEO_RESET_DELAY_MS : remainingMs;
                    try {
                        candidate = await this.waitForNextKeyframe(nextFrameWaitMs);
                    } catch (error) {
                        if (!shouldTryVideoReset) {
                            if (videoResetWritten) throw this.createVideoResetFrameTimeoutError();
                            throw error;
                        }
                        videoResetAttempted = true;
                        try {
                            await this.requestVideoReset(deadline);
                            videoResetWritten = true;
                            deadline = Date.now() + this.options.videoResetFrameTimeoutMs;
                            debugScrcpy(`Scrcpy video reset control write completed; waiting up to ${this.options.videoResetFrameTimeoutMs}ms for a post-reset frame`);
                        } catch (resetError) {
                            debugScrcpy(`Unable to request scrcpy video reset: ${resetError}`);
                        }
                        candidate = this.getCachedKeyframeCandidate();
                    }
                }
            }
            createVideoResetFrameTimeoutError() {
                const timeoutMs = this.options.videoResetFrameTimeoutMs;
                const dataPacketsSeen = this.videoResetState?.dataPacketsSeen ?? 0;
                const observation = 0 === dataPacketsSeen ? 'no data packets were observed while reset recovery was active' : `${dataPacketsSeen} data packet${1 === dataPacketsSeen ? ' was' : 's were'} observed while reset recovery was active`;
                const message = `Scrcpy video reset produced no usable fresh keyframe within the ${timeoutMs}ms post-reset window; ${observation}`;
                debugScrcpy(message);
                return new ScrcpyFrameWaitTimeoutError(message, timeoutMs);
            }
            async closeStaleStreamAndCreateFallbackError(error) {
                const failureKind = this.streamStartupWindow && null === this.frameFreshnessBarrierPtsUs ? 'stream-startup' : 'freshness-target';
                const timeoutMs = error instanceof ScrcpyFrameWaitTimeoutError ? error.timeoutMs : 'stream-startup' === failureKind ? MAX_KEYFRAME_WAIT_MS : FRESH_FRAME_TIMEOUT_MS;
                const causeMessage = error instanceof Error ? error.message : String(error);
                await this.disconnect();
                return new ScrcpyFreshFrameUnavailableError(`Unable to obtain a fresh scrcpy frame; the stale stream epoch was closed so the caller can restart it or use ADB screenshot fallback. ${causeMessage}`, {
                    cause: error,
                    failureKind,
                    timeoutMs,
                    videoBitRate: this.options.videoBitRate
                });
            }
            async getScreenshotJpeg() {
                const perfStart = Date.now();
                const t1 = Date.now();
                await this.ensureConnected();
                const connectTime = Date.now() - t1;
                const t2 = Date.now();
                let frame;
                try {
                    await this.ensureFrameClockCalibration();
                    await this.waitForKeyframe();
                    frame = await this.waitForPlanningFrame();
                } catch (error) {
                    throw await this.closeStaleStreamAndCreateFallbackError(error);
                }
                const frameWaitTime = Date.now() - t2;
                const keyframeBuffer = Buffer.concat([
                    frame.header,
                    frame.data
                ]);
                this.resetIdleTimer();
                debugScrcpy(`Decoding H.264 stream: ${keyframeBuffer.length} bytes (post-barrier)`);
                const t5 = Date.now();
                const result = await this.decodeH264ToJpeg(keyframeBuffer);
                const decodeTime = Date.now() - t5;
                this.recordDecodedFrame(frame, result);
                const totalTime = Date.now() - perfStart;
                debugScrcpy(`Performance: total=${totalTime}ms (connect=${connectTime}ms, frameWait=${frameWaitTime}ms, decode=${decodeTime}ms)`);
                return result;
            }
            getResolution() {
                return this.videoResolution;
            }
            notifyKeyframeWaiters(frame) {
                const resolvers = this.keyframeResolvers;
                this.keyframeResolvers = [];
                for (const resolve of resolvers)resolve(frame);
            }
            waitForNextKeyframe(timeoutMs) {
                return new Promise((resolve, reject)=>{
                    const wrappedResolve = (frame)=>{
                        clearTimeout(timer);
                        resolve(frame);
                    };
                    const timer = setTimeout(()=>{
                        this.keyframeResolvers = this.keyframeResolvers.filter((r)=>r !== wrappedResolve);
                        reject(new Error(`No fresh keyframe received within ${timeoutMs}ms`));
                    }, timeoutMs);
                    this.keyframeResolvers.push(wrappedResolve);
                });
            }
            async ensureFfmpegAvailable() {
                if (null !== this.ffmpegAvailable) return;
                try {
                    this.ffmpegAvailable = await this.checkFfmpegAvailable();
                    if (!this.ffmpegAvailable) debugScrcpy("Warning: ffmpeg is not available. Scrcpy screenshot will be disabled.\nTo enable high-performance screenshots:\n  1. Install optional dependency: pnpm add -D @ffmpeg-installer/ffmpeg\n  2. Or install system ffmpeg: https://ffmpeg.org");
                } catch (error) {
                    this.ffmpegAvailable = false;
                    debugScrcpy(`Error checking ffmpeg availability: ${error}`);
                }
                if (!this.ffmpegAvailable) throw new Error('ffmpeg is not available, please use standard ADB screenshot mode');
            }
            async waitForKeyframe() {
                const startTime = Date.now();
                while(!this.spsHeader && Date.now() - startTime < MAX_KEYFRAME_WAIT_MS){
                    const elapsed = Date.now() - startTime;
                    debugScrcpy(`Waiting for first keyframe (SPS/PPS header)... ${elapsed}ms`);
                    await new Promise((resolve)=>setTimeout(resolve, KEYFRAME_POLL_INTERVAL_MS));
                }
                if (!this.spsHeader) throw new Error(`No keyframe received within ${MAX_KEYFRAME_WAIT_MS}ms. Device may have a long GOP interval or video encoding issues. Please retry.`);
            }
            async checkFfmpegAvailable() {
                const { execFile } = await import("node:child_process");
                const { promisify } = await import("node:util");
                const execFileAsync = promisify(execFile);
                try {
                    const ffmpegPath = this.getFfmpegPath();
                    await execFileAsync(ffmpegPath, [
                        '-version'
                    ]);
                    debugScrcpy(`ffmpeg is available at: ${ffmpegPath}`);
                    return true;
                } catch (error) {
                    debugScrcpy(`ffmpeg is not available: ${error}`);
                    return false;
                }
            }
            async decodeH264ToJpeg(h264Buffer) {
                const { spawn } = await import("node:child_process");
                return new Promise((resolve, reject)=>{
                    const ffmpegArgs = [
                        '-f',
                        'h264',
                        '-i',
                        'pipe:0',
                        '-vframes',
                        '1',
                        '-f',
                        'image2pipe',
                        '-vcodec',
                        'mjpeg',
                        '-q:v',
                        '5',
                        '-loglevel',
                        'error',
                        'pipe:1'
                    ];
                    const ffmpegPath = this.getFfmpegPath();
                    const ffmpeg = spawn(ffmpegPath, ffmpegArgs, {
                        stdio: [
                            'pipe',
                            'pipe',
                            'pipe'
                        ]
                    });
                    const chunks = [];
                    let stderrOutput = '';
                    ffmpeg.stdout.on('data', (chunk)=>{
                        chunks.push(chunk);
                    });
                    ffmpeg.stderr.on('data', (data)=>{
                        stderrOutput += data.toString();
                    });
                    ffmpeg.on('close', (code)=>{
                        if (0 === code && chunks.length > 0) {
                            const jpegBuffer = Buffer.concat(chunks);
                            debugScrcpy(`FFmpeg decode successful, JPEG size: ${jpegBuffer.length} bytes`);
                            resolve(jpegBuffer);
                        } else {
                            const errorMsg = stderrOutput || `FFmpeg exited with code ${code}`;
                            debugScrcpy(`FFmpeg decode failed: ${errorMsg}`);
                            reject(new Error(`H.264 to JPEG decode failed: ${errorMsg}`));
                        }
                    });
                    ffmpeg.on('error', (error)=>{
                        reject(new Error(`Failed to spawn ffmpeg process: ${error.message}`));
                    });
                    ffmpeg.stdin.write(h264Buffer);
                    ffmpeg.stdin.end();
                });
            }
            resetIdleTimer() {
                if (this.idleTimer) {
                    clearTimeout(this.idleTimer);
                    this.idleTimer = null;
                }
                if (!this.options.idleTimeoutMs) return;
                if (this.keyframeListeners.size > 0) return;
                this.idleTimer = setTimeout(()=>{
                    debugScrcpy('Idle timeout reached, disconnecting scrcpy');
                    this.disconnect();
                }, this.options.idleTimeoutMs);
            }
            async disconnect() {
                debugScrcpy('Disconnecting scrcpy...');
                if (this.idleTimer) {
                    clearTimeout(this.idleTimer);
                    this.idleTimer = null;
                }
                const client = this.scrcpyClient;
                const reader = this.streamReader;
                this.scrcpyClient = null;
                this.videoStream = null;
                this.streamReader = null;
                this.spsHeader = null;
                this.clearFrameCache();
                this.isInitialized = false;
                this.keyframeResolvers = [];
                this.keyframeListeners.clear();
                this.resetFrameFreshnessState();
                this.advanceStreamEpoch();
                this.streamReaderEpoch++;
                if (reader) try {
                    await reader.cancel();
                } catch (error) {
                    debugScrcpy(`Error cancelling scrcpy stream reader: ${error}`);
                }
                if (client) try {
                    await client.close();
                } catch (error) {
                    debugScrcpy(`Error closing scrcpy client: ${error}`);
                }
                debugScrcpy('Scrcpy disconnected');
            }
            async dispose() {
                if (this.disposePromise) return this.disposePromise;
                this.disposed = true;
                const disposePromise = (async ()=>{
                    if (this.connectionPromise) await this.connectionPromise.catch(()=>{});
                    await this.disconnect();
                    await this.adb.close();
                })();
                this.disposePromise = disposePromise;
                return disposePromise;
            }
            isConnected() {
                return this.isInitialized && null !== this.scrcpyClient;
            }
            constructor(adb, pushServer, options = {}){
                _define_property(this, "pushServer", void 0);
                _define_property(this, "adb", void 0);
                _define_property(this, "scrcpyClient", void 0);
                _define_property(this, "videoStream", void 0);
                _define_property(this, "spsHeader", void 0);
                _define_property(this, "idleTimer", void 0);
                _define_property(this, "connectionPromise", void 0);
                _define_property(this, "disposed", void 0);
                _define_property(this, "isInitialized", void 0);
                _define_property(this, "options", void 0);
                _define_property(this, "ffmpegAvailable", void 0);
                _define_property(this, "keyframeResolvers", void 0);
                _define_property(this, "keyframeListeners", void 0);
                _define_property(this, "lastRawKeyframe", void 0);
                _define_property(this, "lastRawKeyframeAt", void 0);
                _define_property(this, "lastRawKeyframePtsUs", void 0);
                _define_property(this, "lastRawKeyframeEstimatedAgeMs", void 0);
                _define_property(this, "lastRawKeyframeStreamEpoch", void 0);
                _define_property(this, "streamEpoch", void 0);
                _define_property(this, "streamReaderEpoch", void 0);
                _define_property(this, "videoResolution", void 0);
                _define_property(this, "streamReader", void 0);
                _define_property(this, "frameFreshnessBarrierPtsUs", void 0);
                _define_property(this, "frameFreshnessBarrierReason", void 0);
                _define_property(this, "streamStartupWindow", void 0);
                _define_property(this, "frameFreshnessBarrierPending", void 0);
                _define_property(this, "frameFreshnessBarrierGeneration", void 0);
                _define_property(this, "deviceClockCalibration", void 0);
                _define_property(this, "deviceClockCalibrationPromise", void 0);
                _define_property(this, "hasRetriedUncertainClockCalibration", void 0);
                _define_property(this, "lastFramePtsUs", void 0);
                _define_property(this, "frameFreshnessError", void 0);
                _define_property(this, "lastFrameFreshnessWarningAt", void 0);
                _define_property(this, "hasEstablishedVideoFrame", void 0);
                _define_property(this, "videoResetState", void 0);
                _define_property(this, "disposePromise", void 0);
                this.pushServer = pushServer;
                this.scrcpyClient = null;
                this.videoStream = null;
                this.spsHeader = null;
                this.idleTimer = null;
                this.connectionPromise = null;
                this.disposed = false;
                this.isInitialized = false;
                this.ffmpegAvailable = null;
                this.keyframeResolvers = [];
                this.keyframeListeners = new Set();
                this.lastRawKeyframe = null;
                this.lastRawKeyframeAt = 0;
                this.streamEpoch = Symbol('scrcpy-stream');
                this.streamReaderEpoch = 0;
                this.videoResolution = null;
                this.streamReader = null;
                this.frameFreshnessBarrierPtsUs = null;
                this.frameFreshnessBarrierReason = null;
                this.streamStartupWindow = null;
                this.frameFreshnessBarrierPending = false;
                this.frameFreshnessBarrierGeneration = 0;
                this.deviceClockCalibration = null;
                this.deviceClockCalibrationPromise = null;
                this.hasRetriedUncertainClockCalibration = false;
                this.lastFramePtsUs = null;
                this.frameFreshnessError = null;
                this.lastFrameFreshnessWarningAt = 0;
                this.hasEstablishedVideoFrame = false;
                this.videoResetState = null;
                this.disposePromise = null;
                this.adb = adb;
                const requestedBitRate = options.videoBitRate ?? DEFAULT_VIDEO_BIT_RATE;
                const clampedBitRate = Math.min(requestedBitRate, MAX_VIDEO_BIT_RATE);
                if (requestedBitRate > MAX_VIDEO_BIT_RATE) warnScrcpy(`videoBitRate ${requestedBitRate} exceeds maximum ${MAX_VIDEO_BIT_RATE}, clamped to ${clampedBitRate}`);
                this.options = {
                    maxSize: options.maxSize ?? DEFAULT_MAX_SIZE,
                    videoBitRate: clampedBitRate,
                    idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
                    videoResetFrameTimeoutMs: options.videoResetFrameTimeoutMs ?? DEFAULT_VIDEO_RESET_FRAME_TIMEOUT_MS
                };
            }
        }
    },
    "@midscene/shared/img" (module) {
        module.exports = __rspack_external__midscene_shared_img_758d2833;
    },
    "@midscene/shared/logger" (module) {
        module.exports = __rspack_external__midscene_shared_logger_b1dc2426;
    },
    "node:fs" (module) {
        module.exports = __rspack_external_node_fs_5ea92f0c;
    },
    "node:module" (module) {
        module.exports = __rspack_external_node_module_ab9f2194;
    },
    "node:path" (module) {
        module.exports = __rspack_external_node_path_c5b9b54f;
    }
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (void 0 !== cachedModule) return cachedModule.exports;
    var module = __webpack_module_cache__[moduleId] = {
        exports: {}
    };
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    return module.exports;
}
(()=>{
    __webpack_require__.d = (exports, definition)=>{
        for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key]
        });
    };
})();
(()=>{
    __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
})();
var logger_ = __webpack_require__("@midscene/shared/logger");
const defaultAppNameMapping = {
    微信: 'com.tencent.mm',
    QQ: 'com.tencent.mobileqq',
    微博: 'com.sina.weibo',
    淘宝: 'com.taobao.taobao',
    京东: 'com.jingdong.app.mall',
    拼多多: 'com.xunmeng.pinduoduo',
    淘宝闪购: 'com.taobao.taobao',
    京东秒送: 'com.jingdong.app.mall',
    小红书: 'com.xingin.xhs',
    豆瓣: 'com.douban.frodo',
    知乎: 'com.zhihu.android',
    高德地图: 'com.autonavi.minimap',
    百度地图: 'com.baidu.BaiduMap',
    美团: 'com.sankuai.meituan',
    大众点评: 'com.dianping.v1',
    饿了么: 'me.ele',
    肯德基: 'com.yek.android.kfc.activitys',
    携程: 'ctrip.android.view',
    铁路12306: 'com.MobileTicket',
    12306: 'com.MobileTicket',
    去哪儿旅行: 'com.Qunar',
    滴滴出行: 'com.sdu.didi.psnger',
    bilibili: 'tv.danmaku.bili',
    抖音: 'com.ss.android.ugc.aweme',
    懂车帝: 'com.ss.android.auto',
    快手: 'com.smile.gifmaker',
    腾讯视频: 'com.tencent.qqlive',
    爱奇艺: 'com.qiyi.video',
    优酷视频: 'com.youku.phone',
    芒果TV: 'com.hunantv.imgo.activity',
    红果短剧: 'com.phoenix.read',
    网易云音乐: 'com.netease.cloudmusic',
    QQ音乐: 'com.tencent.qqmusic',
    汽水音乐: 'com.luna.music',
    喜马拉雅: 'com.ximalaya.ting.android',
    番茄小说: 'com.dragon.read',
    七猫免费小说: 'com.kmxs.reader',
    飞书: 'com.ss.android.lark',
    QQ邮箱: 'com.tencent.androidqqmail',
    豆包: 'com.larus.nova',
    Keep: 'com.gotokeep.keep',
    美柚: 'com.lingan.seeyou',
    腾讯新闻: 'com.tencent.news',
    今日头条: 'com.ss.android.article.news',
    贝壳找房: 'com.lianjia.beike',
    安居客: 'com.anjuke.android.app',
    同花顺: 'com.hexin.plat.android',
    星穹铁道: 'com.miHoYo.hkrpg',
    '崩坏：星穹铁道': 'com.miHoYo.hkrpg',
    恋与深空: 'com.papegames.lysk.cn',
    'Android System Settings': 'com.android.settings',
    Settings: 'com.android.settings',
    'Audio Recorder': 'com.android.soundrecorder',
    Clock: 'com.android.deskclock',
    Contacts: 'com.android.contacts',
    Files: 'com.android.fileexplorer',
    'Google Chrome': 'com.android.chrome',
    Gmail: 'com.google.android.gm',
    'Google Files': 'com.google.android.apps.nbu.files',
    'Google Calendar': 'com.google.android.calendar',
    'Google Chat': 'com.google.android.apps.dynamite',
    'Google Clock': 'com.google.android.deskclock',
    'Google Contacts': 'com.google.android.contacts',
    'Google Docs': 'com.google.android.apps.docs.editors.docs',
    'Google Drive': 'com.google.android.apps.docs',
    'Google Fit': 'com.google.android.apps.fitness',
    'Google Keep': 'com.google.android.keep',
    'Google Maps': 'com.google.android.apps.maps',
    'Google Play Books': 'com.google.android.apps.books',
    'Google Play Store': 'com.android.vending',
    'Google Slides': 'com.google.android.apps.docs.editors.slides',
    'Google Tasks': 'com.google.android.apps.tasks',
    Bluecoins: 'com.rammigsoftware.bluecoins',
    Broccoli: 'com.flauschcode.broccoli',
    'Booking.com': 'com.booking',
    Duolingo: 'com.duolingo',
    Expedia: 'com.expedia.bookings',
    Joplin: 'net.cozic.joplin',
    McDonald: 'com.mcdonalds.app',
    Osmand: 'net.osmand',
    PiMusicPlayer: 'com.Project100Pi.themusicplayer',
    Quora: 'com.quora.android',
    Reddit: 'com.reddit.frontpage',
    RetroMusic: 'code.name.monkey.retromusic',
    SimpleCalendarPro: 'com.scientificcalculatorplus.simplecalculator.basiccalculator.mathcalc',
    SimpleSMSMessenger: 'com.simplemobiletools.smsmessenger',
    Telegram: 'org.telegram.messenger',
    Temu: 'com.einnovation.temu',
    TikTok: 'com.zhiliaoapp.musically',
    Twitter: 'com.twitter.android',
    X: 'com.twitter.android',
    VLC: 'org.videolan.vlc',
    WeChat: 'com.tencent.mm',
    WhatsApp: 'com.whatsapp'
};
var external_node_fs_ = __webpack_require__("node:fs");
var external_node_module_ = __webpack_require__("node:module");
var external_node_path_ = __webpack_require__("node:path");
var img_ = __webpack_require__("@midscene/shared/img");
const warnAdb = (0, logger_.getDebug)('android:adb', {
    console: true
});
async function adb_createAndroidAdb({ adbExecTimeout, deviceId, deviceOptions }) {
    const androidAdbPath = deviceOptions?.androidAdbPath || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_PATH);
    const remoteAdbHost = deviceOptions?.remoteAdbHost || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_HOST);
    const remoteAdbPort = deviceOptions?.remoteAdbPort || globalConfigManager.getEnvConfigValue(MIDSCENE_ADB_REMOTE_PORT);
    const adbOptions = {
        udid: deviceId,
        adbExecTimeout,
        remoteAdbHost: remoteAdbHost || void 0,
        remoteAdbPort: remoteAdbPort ? Number(remoteAdbPort) : void 0
    };
    if (androidAdbPath) return new ADB({
        ...adbOptions,
        executable: {
            path: androidAdbPath,
            defaultArgs: []
        }
    });
    const sdkRoot = getSdkRootFromEnv();
    if (sdkRoot) try {
        return await ADB.createADB(adbOptions);
    } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        warnAdb(`Unable to initialize adb from Android SDK at "${sdkRoot}", falling back to adb from PATH: ${message}`);
    }
    return new ADB(adbOptions);
}
const EMPTY_ADB_SHELL_STDOUT = '<empty>';
const MAX_RUN_ADB_SHELL_STDOUT = 200;
function normalizeShellStream(value) {
    if (null == value) return '';
    return String(value);
}
function truncateAdbShellStream(output, streamName) {
    if (output.length <= MAX_RUN_ADB_SHELL_STDOUT) return output;
    return `${output.slice(0, MAX_RUN_ADB_SHELL_STDOUT)}
...[${streamName} truncated, ${output.length - MAX_RUN_ADB_SHELL_STDOUT} more characters]`;
}
function buildRunAdbShellPlanningFeedback({ command, stdout }) {
    if ('' === stdout) return;
    const commandText = 'string' == typeof command && command.length > 0 ? `Command: ${command}\n` : '';
    return `${commandText}Stdout:
${stdout}`;
}
function buildAdbShellStderrErrorMessage(command, stdout, stderr) {
    return `RunAdbShell command returned stderr.
Command: ${command}
Stderr:
${truncateAdbShellStream(stderr, 'stderr')}
Stdout:
${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
}
function getAdbShellStdoutOrThrow(command, output) {
    if ('string' == typeof output) return output;
    const stdout = normalizeShellStream(output.stdout);
    const stderr = normalizeShellStream(output.stderr);
    if (stderr) throw new Error(buildAdbShellStderrErrorMessage(command, stdout, stderr));
    return stdout;
}
async function runAdbShellStdoutOrThrow(adb, command, options = {}) {
    const output = await adb.shell(command, {
        ...options,
        outputFormat: adb.EXEC_OUTPUT_FORMAT.FULL
    });
    return getAdbShellStdoutOrThrow(command, output);
}
var resource_path = __webpack_require__("./src/resource-path.ts");
var scrcpy_manager = __webpack_require__("./src/scrcpy-manager.ts");
function _define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debugAdapter = (0, logger_.getDebug)('android:scrcpy-adapter');
const SCRCPY_RETRY_COOLDOWN_MS = 5000;
function formatScrcpyFreshFrameFailure(error) {
    const videoBitRate = error.videoBitRate;
    const timeoutMs = Number.isFinite(error.timeoutMs) ? error.timeoutMs : 'stream-startup' === error.failureKind ? 5000 : 300;
    if ('stream-startup' === error.failureKind) return `The new scrcpy stream did not produce a usable baseline frame within ${timeoutMs}ms. This is a stream startup or encoder-readiness failure, not evidence that the configured video bitrate is too high.`;
    const bitRateDescription = Number.isFinite(videoBitRate) ? ` Current videoBitRate: ${videoBitRate} bps (${videoBitRate / 1000000} Mbps).` : '';
    return `No usable scrcpy frame crossed the active freshness target within ${timeoutMs}ms. This can happen when a static screen emits no new encoded frame, or when the video stream or transport is interrupted. This timeout does not by itself identify bandwidth or the configured video bitrate as the cause.${bitRateDescription}`;
}
class ScrcpyDeviceAdapter {
    isEnabled() {
        if (!this.isConfigured()) return false;
        return null === this.retryAfter || Date.now() >= this.retryAfter;
    }
    getStatus() {
        return {
            enabled: this.isConfigured(),
            connected: this.manager?.isConnected() ?? false,
            lastError: this.lastError,
            retryAfter: this.retryAfter
        };
    }
    isConfigured() {
        return this.scrcpyConfig?.enabled ?? scrcpy_manager.ov.enabled;
    }
    async initialize(deviceInfo) {
        try {
            const manager = await this.ensureConnectedManager(deviceInfo);
            await this.applyPendingActionBarrier(manager);
            this.clearFailure();
        } catch (error) {
            this.recordFailure(error);
            throw error;
        }
    }
    recordFailure(error) {
        this.lastError = error instanceof Error ? error.message : String(error);
        this.retryAfter = Date.now() + SCRCPY_RETRY_COOLDOWN_MS;
    }
    clearFailure() {
        this.lastError = null;
        this.retryAfter = null;
    }
    ensureRetryReady() {
        if (null === this.retryAfter || Date.now() >= this.retryAfter) return;
        throw new Error(`scrcpy retry is cooling down until ${new Date(this.retryAfter).toISOString()}. Last error: ${this.lastError}`);
    }
    resolveConfig(_deviceInfo) {
        if (this.resolvedConfig) return this.resolvedConfig;
        const config = this.scrcpyConfig;
        const maxSize = config?.maxSize ?? scrcpy_manager.ov.maxSize;
        if (!Number.isInteger(maxSize) || maxSize < 0) throw new Error(`Invalid scrcpyConfig.maxSize: expected a non-negative integer, received ${maxSize}`);
        const videoBitRate = config?.videoBitRate ?? scrcpy_manager.ov.videoBitRate;
        const videoResetFrameTimeoutMs = config?.videoResetFrameTimeoutMs ?? scrcpy_manager.ov.videoResetFrameTimeoutMs;
        if (!Number.isInteger(videoResetFrameTimeoutMs) || videoResetFrameTimeoutMs <= 0) throw new Error(`Invalid scrcpyConfig.videoResetFrameTimeoutMs: expected a positive integer, received ${videoResetFrameTimeoutMs}`);
        this.resolvedConfig = {
            enabled: this.isConfigured(),
            maxSize,
            idleTimeoutMs: config?.idleTimeoutMs ?? scrcpy_manager.ov.idleTimeoutMs,
            videoBitRate,
            videoResetFrameTimeoutMs
        };
        return this.resolvedConfig;
    }
    async ensureManager(_deviceInfo) {
        if (this.manager) return this.manager;
        if (this.managerPromise) return this.managerPromise;
        debugAdapter('Initializing Scrcpy manager...');
        const generation = this.lifecycleGeneration;
        const managerPromise = (async ()=>{
            let adb = null;
            let manager = null;
            try {
                const { Adb, AdbServerClient } = await import("@yume-chan/adb");
                const { AdbServerNodeTcpConnector } = await import("@yume-chan/adb-server-node-tcp");
                const { ScrcpyScreenshotManager: ScrcpyManager } = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, "./src/scrcpy-manager.ts"));
                const adbBackend = await this.resolveAdbBackend();
                const adbClient = new AdbServerClient(new AdbServerNodeTcpConnector({
                    host: adbBackend.adbHost ?? '127.0.0.1',
                    port: adbBackend.adbPort ?? 5037
                }));
                adb = new Adb(await adbClient.createTransport({
                    serial: this.deviceId
                }));
                const config = this.resolveConfig();
                manager = new ScrcpyManager(adb, async (localPath, remotePath)=>{
                    await adbBackend.push(localPath, remotePath);
                }, {
                    maxSize: config.maxSize,
                    videoBitRate: config.videoBitRate,
                    idleTimeoutMs: config.idleTimeoutMs,
                    videoResetFrameTimeoutMs: config.videoResetFrameTimeoutMs
                });
                await manager.validateEnvironment();
                if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy manager initialization was superseded by device cleanup');
                this.manager = manager;
                debugAdapter('Scrcpy manager initialized');
                return manager;
            } catch (error) {
                try {
                    if (manager) await manager.dispose();
                    else await adb?.close();
                } catch (cleanupError) {
                    debugAdapter(`Failed to clean up Scrcpy manager initialization: ${cleanupError}`);
                }
                debugAdapter(`Failed to initialize Scrcpy manager: ${error}`);
                throw new Error(`Failed to initialize Scrcpy for device ${this.deviceId}. Ensure ADB server is running and device is connected. Error: ${error}`);
            }
        })();
        this.managerPromise = managerPromise;
        try {
            return await managerPromise;
        } finally{
            if (this.managerPromise === managerPromise) this.managerPromise = null;
        }
    }
    async ensureConnectedManager(deviceInfo) {
        const manager = await this.ensureManager(deviceInfo);
        const wasConnected = manager.isConnected();
        await manager.ensureConnected();
        this.attachKeyframeListeners(manager, !wasConnected);
        return manager;
    }
    async screenshotBase64(deviceInfo) {
        this.ensureRetryReady();
        try {
            const manager = await this.ensureConnectedManager(deviceInfo);
            await this.applyPendingActionBarrier(manager);
            const screenshotBuffer = await manager.getScreenshotJpeg();
            this.clearFailure();
            return this.jpegBufferToBase64(screenshotBuffer);
        } catch (error) {
            if (!(0, scrcpy_manager.GJ)(error)) {
                this.recordFailure(error);
                throw error;
            }
            debugAdapter(`Scrcpy freshness target was unavailable; restarting the stream once before ADB fallback: ${error}`);
            try {
                const screenshotBuffer = await this.restartAndCaptureOnce(deviceInfo);
                this.clearFailure();
                debugAdapter('Scrcpy screenshot recovered on a new stream epoch');
                return this.jpegBufferToBase64(screenshotBuffer);
            } catch (retryError) {
                this.keyframeUnsubscribers.clear();
                this.recordFailure(retryError);
                throw retryError;
            }
        }
    }
    async restartAndCaptureOnce(deviceInfo) {
        if (this.freshnessRestartPromise) return this.freshnessRestartPromise;
        const generation = this.lifecycleGeneration;
        const restartPromise = (async ()=>{
            const manager = await this.ensureConnectedManager(deviceInfo);
            await this.applyPendingActionBarrier(manager);
            if (generation !== this.lifecycleGeneration) throw new Error('Scrcpy freshness restart was cancelled by cleanup');
            return manager.getScreenshotJpeg();
        })();
        this.freshnessRestartPromise = restartPromise;
        try {
            return await restartPromise;
        } finally{
            if (this.freshnessRestartPromise === restartPromise) this.freshnessRestartPromise = null;
        }
    }
    jpegBufferToBase64(screenshotBuffer) {
        return (0, img_.createImgBase64ByFormat)('jpeg', screenshotBuffer.toString('base64'));
    }
    async subscribeKeyframes(deviceInfo, listener) {
        this.ensureRetryReady();
        this.keyframeListeners.add(listener);
        try {
            const manager = await this.ensureConnectedManager(deviceInfo);
            await this.applyPendingActionBarrier(manager);
            await manager.ensureFrameClockCalibration();
            this.clearFailure();
            return ()=>{
                this.keyframeListeners.delete(listener);
                this.keyframeUnsubscribers.get(listener)?.unsubscribe();
                this.keyframeUnsubscribers.delete(listener);
            };
        } catch (error) {
            this.keyframeListeners.delete(listener);
            this.keyframeUnsubscribers.get(listener)?.unsubscribe();
            this.keyframeUnsubscribers.delete(listener);
            this.recordFailure(error);
            throw error;
        }
    }
    getLatestRawKeyframe() {
        return this.manager?.getLatestRawKeyframe() ?? null;
    }
    attachKeyframeListener(manager, listener, force) {
        const existing = this.keyframeUnsubscribers.get(listener);
        if (!force && existing?.manager === manager) return;
        existing?.unsubscribe();
        this.keyframeUnsubscribers.set(listener, {
            manager,
            unsubscribe: manager.subscribeKeyframes(listener)
        });
    }
    attachKeyframeListeners(manager, force) {
        for (const listener of this.keyframeListeners)this.attachKeyframeListener(manager, listener, force);
    }
    async applyPendingActionBarrier(manager) {
        const actionCompletedAtHostUs = this.pendingActionBarrierAtHostUs;
        if (null === actionCompletedAtHostUs) return;
        await manager.setFreshnessBarrier('completed input action while scrcpy was unavailable', {
            hostMonotonicUs: actionCompletedAtHostUs
        });
        if (this.pendingActionBarrierAtHostUs === actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
    }
    monotonicTimeUs() {
        return process.hrtime.bigint() / 1000n;
    }
    deferActionBarrier(actionCompletedAtHostUs) {
        if (null === this.pendingActionBarrierAtHostUs || actionCompletedAtHostUs > this.pendingActionBarrierAtHostUs) this.pendingActionBarrierAtHostUs = actionCompletedAtHostUs;
    }
    async markActionBarrier() {
        const actionCompletedAtHostUs = this.monotonicTimeUs();
        const manager = this.manager;
        if (!manager?.isConnected()) return void this.deferActionBarrier(actionCompletedAtHostUs);
        try {
            await manager.setFreshnessBarrier('completed input action', {
                hostMonotonicUs: actionCompletedAtHostUs
            });
            if (null !== this.pendingActionBarrierAtHostUs && this.pendingActionBarrierAtHostUs <= actionCompletedAtHostUs) this.pendingActionBarrierAtHostUs = null;
            this.clearFailure();
        } catch (error) {
            this.deferActionBarrier(actionCompletedAtHostUs);
            this.recordFailure(error);
            debugAdapter(`Unable to mark scrcpy action barrier; disabling this stream: ${error}`);
            try {
                await manager.dispose();
            } catch (disconnectError) {
                debugAdapter(`Error disconnecting scrcpy after barrier failure: ${disconnectError}`);
            } finally{
                if (this.manager === manager) this.manager = null;
            }
        }
    }
    async decodeRawKeyframeToJpegBase64(frame) {
        if (!this.manager) throw new Error('scrcpy manager is not initialized');
        const jpegBuffer = await this.manager.decodeRawKeyframeToJpeg(frame);
        return (0, img_.createImgBase64ByFormat)('jpeg', jpegBuffer.toString('base64'));
    }
    getResolution() {
        return this.manager?.getResolution() ?? null;
    }
    getSize(deviceInfo) {
        const resolution = this.getResolution();
        if (!resolution) return null;
        debugAdapter(`Using scrcpy resolution: ${resolution.width}x${resolution.height}`);
        return {
            width: resolution.width,
            height: resolution.height
        };
    }
    getScalingRatio(physicalWidth) {
        const resolution = this.getResolution();
        if (!resolution) return null;
        return resolution.width / physicalWidth;
    }
    async disconnect() {
        this.lifecycleGeneration += 1;
        this.pendingActionBarrierAtHostUs = null;
        for (const { unsubscribe } of this.keyframeUnsubscribers.values())unsubscribe();
        this.keyframeUnsubscribers.clear();
        this.keyframeListeners.clear();
        if (this.freshnessRestartPromise) await this.freshnessRestartPromise.catch(()=>{});
        if (this.managerPromise) await this.managerPromise.catch(()=>{});
        if (this.manager) {
            try {
                await this.manager.dispose();
            } catch (error) {
                debugAdapter(`Error disconnecting scrcpy: ${error}`);
            }
            this.manager = null;
        }
        this.resolvedConfig = null;
        this.clearFailure();
    }
    constructor(deviceId, scrcpyConfig, resolveAdbBackend = ()=>adb_createAndroidAdb({
            adbExecTimeout: 60000,
            deviceId
        })){
        _define_property(this, "deviceId", void 0);
        _define_property(this, "scrcpyConfig", void 0);
        _define_property(this, "resolveAdbBackend", void 0);
        _define_property(this, "manager", void 0);
        _define_property(this, "managerPromise", void 0);
        _define_property(this, "resolvedConfig", void 0);
        _define_property(this, "lastError", void 0);
        _define_property(this, "retryAfter", void 0);
        _define_property(this, "freshnessRestartPromise", void 0);
        _define_property(this, "lifecycleGeneration", void 0);
        _define_property(this, "pendingActionBarrierAtHostUs", void 0);
        _define_property(this, "keyframeListeners", void 0);
        _define_property(this, "keyframeUnsubscribers", void 0);
        this.deviceId = deviceId;
        this.scrcpyConfig = scrcpyConfig;
        this.resolveAdbBackend = resolveAdbBackend;
        this.manager = null;
        this.managerPromise = null;
        this.resolvedConfig = null;
        this.lastError = null;
        this.retryAfter = null;
        this.freshnessRestartPromise = null;
        this.lifecycleGeneration = 0;
        this.pendingActionBarrierAtHostUs = null;
        this.keyframeListeners = new Set();
        this.keyframeUnsubscribers = new Map();
    }
}
const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
const ATTR_RE = /([A-Za-z_][A-Za-z0-9_.\-:]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
const BOUNDS_RE = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
const ENTITY_TABLE = {
    amp: '&',
    lt: '<',
    gt: '>',
    quot: '"',
    apos: "'"
};
function decodeEntities(value) {
    if (!value.includes('&')) return value;
    return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (full, body)=>{
        if (body.startsWith('#x') || body.startsWith('#X')) {
            const code = Number.parseInt(body.slice(2), 16);
            return Number.isFinite(code) ? String.fromCodePoint(code) : full;
        }
        if (body.startsWith('#')) {
            const code = Number.parseInt(body.slice(1), 10);
            return Number.isFinite(code) ? String.fromCodePoint(code) : full;
        }
        return ENTITY_TABLE[body] ?? full;
    });
}
function parseAttrs(raw) {
    const attrs = {};
    ATTR_RE.lastIndex = 0;
    let match;
    while(match = ATTR_RE.exec(raw))attrs[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
    return attrs;
}
function parseXml(input) {
    if (0 === input.trim().length) throw new Error('parseAndroidUITreeXml: input is empty');
    const cleaned = input.replace(/<\?xml[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
    TAG_RE.lastIndex = 0;
    const stack = [];
    let root;
    let match;
    while(match = TAG_RE.exec(cleaned)){
        const isClose = '/' === match[1];
        const name = match[2];
        if (isClose) {
            const current = stack.pop();
            if (!current || current.name !== name) throw new Error(`parseAndroidUITreeXml: unbalanced close tag </${name}>`);
            continue;
        }
        const element = {
            name,
            attrs: parseAttrs(match[3] ?? ''),
            children: []
        };
        const parent = stack[stack.length - 1];
        if (parent) parent.children.push(element);
        else if (root) throw new Error('parseAndroidUITreeXml: more than one top-level element');
        else root = element;
        if ('/' !== match[4]) stack.push(element);
    }
    if (stack.length > 0) throw new Error('parseAndroidUITreeXml: unclosed element');
    if (!root) throw new Error('parseAndroidUITreeXml: no root element');
    return root;
}
function parseBounds(raw, devicePixelRatio) {
    const match = raw ? BOUNDS_RE.exec(raw) : void 0;
    if (!match) return {
        left: 0,
        top: 0,
        width: 0,
        height: 0
    };
    const left = Number.parseInt(match[1], 10);
    const top = Number.parseInt(match[2], 10);
    const right = Number.parseInt(match[3], 10);
    const bottom = Number.parseInt(match[4], 10);
    const ratio = devicePixelRatio > 0 ? devicePixelRatio : 1;
    return {
        left: left / ratio,
        top: top / ratio,
        width: Math.max(0, right - left) / ratio,
        height: Math.max(0, bottom - top) / ratio
    };
}
function toUiNode(element, devicePixelRatio) {
    const attrs = {};
    for (const [name, value] of Object.entries(element.attrs))if ('bounds' !== name) attrs[name] = value;
    return {
        type: element.attrs.class || element.name,
        attrs,
        bounds: parseBounds(element.attrs.bounds, devicePixelRatio),
        children: element.children.map((child)=>toUiNode(child, devicePixelRatio))
    };
}
function uiautomatorXmlToUiNode(xml, devicePixelRatio) {
    const root = parseXml(xml);
    if ('hierarchy' === root.name && 1 === root.children.length) return toUiNode(root.children[0], devicePixelRatio);
    return toUiNode(root, devicePixelRatio);
}
const ACCESSIBILITY_DUMP_ATTEMPTS = 3;
const ACCESSIBILITY_DUMP_TIMEOUT_MS = 5000;
const ACCESSIBILITY_DUMP_RETRY_DELAY_MS = 250;
const UIAUTOMATOR_LAYOUT_PATH = '/sdcard/midscene_window_dump.xml';
const debugUITree = (0, logger_.getDebug)('android:ui-tree');
function errorMessage(error) {
    return error instanceof Error ? error.message : String(error);
}
function validateHierarchyXml(xml) {
    if ('string' != typeof xml || !/<hierarchy(?:\s|>)/.test(xml) || !xml.includes('</hierarchy>')) throw new Error('uiautomator did not produce valid hierarchy XML');
}
function focusedPackageForLogicalDisplay(windowDump, logicalDisplayId) {
    const displayMatches = [
        ...windowDump.matchAll(/^\s*Display: mDisplayId=(\d+)\b.*$/gm)
    ];
    const displayIndex = displayMatches.findIndex((match)=>Number(match[1]) === logicalDisplayId);
    if (-1 === displayIndex) return null;
    const start = displayMatches[displayIndex].index ?? 0;
    const end = displayMatches[displayIndex + 1]?.index ?? windowDump.length;
    const displayBlock = windowDump.slice(start, end);
    for (const field of [
        'mCurrentFocus',
        'mFocusedApp'
    ]){
        const value = displayBlock.match(new RegExp(`^\\s*${field}=([^\\n]*)$`, 'm'))?.[1];
        const packageName = value?.match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
        if (packageName) return packageName;
    }
    return null;
}
function firstTreePackage(root) {
    if (root.attrs.package) return root.attrs.package;
    for (const child of root.children){
        const packageName = firstTreePackage(child);
        if (packageName) return packageName;
    }
    return null;
}
function windowDisplayIdsForPackage(windowDump, packageName) {
    const windowMatches = [
        ...windowDump.matchAll(/^\s*Window #\d+ Window\{.*$/gm)
    ];
    const displayIds = new Set();
    for(let index = 0; index < windowMatches.length; index++){
        const start = windowMatches[index].index ?? 0;
        const end = windowMatches[index + 1]?.index ?? windowDump.length;
        const windowBlock = windowDump.slice(start, end);
        const ownerPackage = windowBlock.match(/^\s*mOwnerUid=.*\bpackage=([A-Za-z0-9_$.-]+)\b/m)?.[1];
        const headerPackage = windowMatches[index][0].match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
        if (ownerPackage !== packageName && headerPackage !== packageName) continue;
        const displayId = windowBlock.match(/^\s*mDisplayId=(\d+)\b/m)?.[1];
        if (void 0 !== displayId) displayIds.add(Number(displayId));
    }
    return [
        ...displayIds
    ];
}
function uiTreeExtent(root) {
    let right = root.bounds.left + root.bounds.width;
    let bottom = root.bounds.top + root.bounds.height;
    for (const child of root.children){
        const childExtent = uiTreeExtent(child);
        right = Math.max(right, childExtent.right);
        bottom = Math.max(bottom, childExtent.bottom);
    }
    return {
        right,
        bottom
    };
}
async function dumpAccessibilityXml(adb) {
    await runAdbShellStdoutOrThrow(adb, `rm -f ${UIAUTOMATOR_LAYOUT_PATH}`, {
        timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
    });
    await runAdbShellStdoutOrThrow(adb, `uiautomator dump --compressed ${UIAUTOMATOR_LAYOUT_PATH}`, {
        timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
    });
    const xml = await runAdbShellStdoutOrThrow(adb, `cat ${UIAUTOMATOR_LAYOUT_PATH}`, {
        timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
    });
    validateHierarchyXml(xml);
    return xml;
}
async function validateUITreeDisplay(root, options) {
    if ('number' != typeof options.displayId) return;
    const { adb, displayId } = options;
    const displayWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window displays', {
        timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
    });
    const expectedPackage = focusedPackageForLogicalDisplay(displayWindowDump, displayId);
    const treePackage = firstTreePackage(root);
    if (!expectedPackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no focused package found on the target display`);
    if (!treePackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: accessibility hierarchy has no package`);
    const allWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window windows', {
        timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
    });
    const treePackageDisplayIds = windowDisplayIdsForPackage(allWindowDump, treePackage);
    if (0 === treePackageDisplayIds.length) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no window display ownership found for accessibility package ${treePackage}`);
    if (!treePackageDisplayIds.includes(displayId)) throw new Error(`Android UI tree display mismatch for displayId=${displayId}: expected focused package ${expectedPackage}, but accessibility hierarchy belongs to ${treePackage} on displayId=${treePackageDisplayIds.join(',')}`);
    const targetSize = await options.getDisplaySize();
    const extent = uiTreeExtent(root);
    const tolerance = 1;
    if (extent.right > targetSize.width + tolerance || extent.bottom > targetSize.height + tolerance) throw new Error(`Android UI tree bounds exceed displayId=${displayId}: tree ${Math.round(extent.right)}x${Math.round(extent.bottom)} is outside target ${targetSize.width}x${targetSize.height}`);
}
async function captureAndroidUITree(options) {
    const failures = [];
    for(let attempt = 1; attempt <= ACCESSIBILITY_DUMP_ATTEMPTS; attempt++){
        const startedAt = Date.now();
        try {
            const xml = await dumpAccessibilityXml(options.adb);
            const capturedAt = Date.now();
            const root = uiautomatorXmlToUiNode(xml, options.devicePixelRatio);
            await validateUITreeDisplay(root, options);
            debugUITree('capture source=%s attempt=%d durationMs=%d bytes=%d', 'uiautomator', attempt, Date.now() - startedAt, xml.length);
            return {
                platform: 'android',
                capturedAt,
                root
            };
        } catch (error) {
            const failure = `uiautomator attempt ${attempt}/${ACCESSIBILITY_DUMP_ATTEMPTS}: ${errorMessage(error)}`;
            failures.push(failure);
            debugUITree('capture failed source=%s attempt=%d/%d durationMs=%d error=%s', 'uiautomator', attempt, ACCESSIBILITY_DUMP_ATTEMPTS, Date.now() - startedAt, errorMessage(error));
            if (attempt < ACCESSIBILITY_DUMP_ATTEMPTS) await sleep(ACCESSIBILITY_DUMP_RETRY_DELAY_MS);
        }
    }
    throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
}
function createVisualActionRegistry(definitions, onActionSettled) {
    const registeredActions = {};
    for (const actionName of Object.keys(definitions)){
        const dispatch = definitions[actionName];
        registeredActions[actionName] = async (...args)=>{
            try {
                return await dispatch(...args);
            } finally{
                await onActionSettled(actionName);
            }
        };
    }
    return registeredActions;
}
function device_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const defaultScrollUntilTimes = 10;
const defaultFastScrollDuration = 100;
const defaultNormalScrollDuration = 1000;
const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
const androidTextClearKeyRepeatCount = 100;
const androidClearInputKeyCodes = {
    backwardDelete: 67,
    forwardDelete: 112,
    moveEnd: 123
};
const SCREENSHOT_STRATEGY_ALWAYS_YADB = 'always-yadb';
const SCREENSHOT_STRATEGY_AUTO = 'auto';
const debugDevice = (0, logger_.getDebug)('android:device');
const warnDevice = (0, logger_.getDebug)('android:device', {
    console: true
});
function displayViewportForLogicalDisplay(displayDump, logicalDisplayId) {
    for (const viewportMatch of displayDump.matchAll(/DisplayViewport\{([^}]*)\}/g)){
        const viewport = viewportMatch[1];
        const displayId = viewport.match(/\bdisplayId=(\d+)\b/)?.[1];
        if (Number(displayId) === logicalDisplayId) return viewportMatch[0];
    }
    return null;
}
function displayInfoLineForPhysicalDisplay(displayDump, physicalDisplayId) {
    const lineRegex = new RegExp(`^.*uniqueId ["']local:${physicalDisplayId}["'].*$`, 'm');
    return displayDump.match(lineRegex)?.[0] ?? null;
}
function physicalDisplayIdForLogicalDisplay(displayDump, logicalDisplayId) {
    const viewport = displayViewportForLogicalDisplay(displayDump, logicalDisplayId);
    if (viewport) {
        const physicalId = viewport.match(/\buniqueId=['"]local:(\d+)['"]/)?.[1];
        if (physicalId) return physicalId;
    }
    const lines = displayDump.split(/\r?\n/);
    for(let index = 0; index < lines.length; index++){
        const displayId = lines[index].match(/^\s*mDisplayId=(\d+)\b/)?.[1];
        if (Number(displayId) === logicalDisplayId) for(let cursor = index + 1; cursor < lines.length; cursor++){
            if (/^\s*mDisplayId=\d+\b/.test(lines[cursor])) break;
            const physicalId = lines[cursor].match(/mBaseDisplayInfo=.*\buniqueId "local:(\d+)"/)?.[1];
            if (physicalId) return physicalId;
        }
    }
    return null;
}
function escapeForShell(text) {
    return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
}
function shellEscapeArg(text) {
    const escaped = text.replace(/'/g, "'\\''");
    return `'${escaped}'`;
}
class AndroidDevice {
    actionSpace() {
        const mobileActionContext = {
            input: this.inputPrimitives,
            size: ()=>this.size(),
            sleep: async (timeMs)=>{
                await sleep(timeMs);
            },
            getDefaultAutoDismissKeyboard: ()=>this.options?.autoDismissKeyboard,
            systemActions: {
                backButton: {
                    name: 'AndroidBackButton',
                    description: 'Trigger the system "back" operation on Android devices'
                },
                homeButton: {
                    name: 'AndroidHomeButton',
                    description: 'Trigger the system "home" operation on Android devices'
                },
                recentAppsButton: {
                    name: 'AndroidRecentAppsButton',
                    description: 'Trigger the system "recent apps" operation on Android devices'
                }
            }
        };
        const defaultActions = [
            ...createDefaultMobileActions(mobileActionContext),
            defineAction({
                name: 'PullGesture',
                description: 'Trigger pull down to refresh or pull up actions',
                paramSchema: z.object({
                    direction: z["enum"]([
                        'up',
                        'down'
                    ]).describe('The direction to pull'),
                    distance: z.number().optional().describe('The distance to pull (in pixels)'),
                    duration: z.number().optional().describe('The duration of the pull (in milliseconds)'),
                    locate: getMidsceneLocationSchema().optional().describe('The element to start the pull from (optional)')
                }),
                sample: {
                    direction: 'down',
                    locate: {
                        prompt: 'the center of the content list area'
                    }
                },
                call: async (param)=>{
                    if (!param || !param.direction) throw new Error('PullGesture requires a direction parameter');
                    await this.visualActions.pullGesture(param);
                }
            })
        ];
        const platformActions = createPlatformActions(this.visualActions);
        let platformSpecificActions = Object.values(platformActions);
        if (this.options?.exposeRunAdbShellAction === false) platformSpecificActions = platformSpecificActions.filter((action)=>action !== platformActions.RunAdbShell);
        const customActions = this.customActions || [];
        return [
            ...defaultActions,
            ...platformSpecificActions,
            ...customActions
        ];
    }
    async performPullGesture(param) {
        const element = param.locate;
        const startPoint = element ? {
            left: element.center[0],
            top: element.center[1]
        } : void 0;
        if ('down' === param.direction) await this.pullDownRaw(startPoint, param.distance, param.duration);
        else if ('up' === param.direction) await this.pullUpRaw(startPoint, param.distance, param.duration);
        else throw new Error(`Unknown pull direction: ${param.direction}`);
    }
    async performActionScroll(param) {
        const element = param.locate;
        const startingPoint = element ? {
            left: element.center[0],
            top: element.center[1]
        } : void 0;
        const scrollToEventName = param?.scrollType;
        if ('scrollToTop' === scrollToEventName) await this.scrollUntilTopRaw(startingPoint);
        else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottomRaw(startingPoint);
        else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRightRaw(startingPoint);
        else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeftRaw(startingPoint);
        else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
        else {
            if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUpRaw(param.distance || void 0, startingPoint);
            else if ('left' === param.direction) await this.scrollLeftRaw(param.distance || void 0, startingPoint);
            else if ('right' === param.direction) await this.scrollRightRaw(param.distance || void 0, startingPoint);
            else throw new Error(`Unknown scroll direction: ${param.direction}`);
            else await this.scrollDownRaw(param?.distance || void 0, startingPoint);
            await sleep(500);
        }
    }
    async runAdbShellRaw(param, context) {
        const adb = await this.getAdb();
        const stdout = await runAdbShellStdoutOrThrow(adb, param.command, void 0 === param.timeout ? void 0 : {
            timeout: param.timeout
        });
        const planningFeedback = buildRunAdbShellPlanningFeedback({
            command: param.command,
            stdout
        });
        if (planningFeedback && context?.task) context.task.planningFeedback = planningFeedback;
        return stdout;
    }
    describe() {
        return this.description || `DeviceId: ${this.deviceId}`;
    }
    async connect() {
        const adb = await this.getAdb();
        const adapter = this.getScrcpyAdapter();
        if (adapter.isEnabled()) try {
            const deviceInfo = await this.getDevicePhysicalInfo();
            await adapter.initialize(deviceInfo);
            console.log(`[midscene] Using scrcpy for screenshots (device: ${this.deviceId})`);
        } catch (error) {
            const msg = error instanceof Error ? error.message : String(error);
            warnDevice(`[midscene] Scrcpy unavailable, using ADB fallback (device: ${this.deviceId}): ${msg}. Call retryScrcpy() to retry immediately.`);
        }
        return adb;
    }
    async getAdb() {
        if (this.destroyed) throw new Error(`AndroidDevice ${this.deviceId} has been destroyed and cannot execute ADB commands`);
        if (this.adb) return this.createAdbProxy(this.adb);
        if (this.connectingAdb) return this.connectingAdb.then((adb)=>this.createAdbProxy(adb));
        this.connectingAdb = (async ()=>{
            let error = null;
            debugDevice(`Initializing ADB with device ID: ${this.deviceId}`);
            try {
                this.adb = await adb_createAndroidAdb({
                    adbExecTimeout: 60000,
                    deviceId: this.deviceId,
                    deviceOptions: this.options
                });
                const size = await this.getScreenSize();
                this.description = `
DeviceId: ${this.deviceId}
ScreenSize:
${Object.keys(size).filter((key)=>size[key]).map((key)=>`  ${key} size: ${size[key]}${'override' === key && size[key] ? ' ✅' : ''}`).join('\n')}
`;
                debugDevice('ADB initialized successfully', this.description);
                return this.adb;
            } catch (e) {
                debugDevice(`Failed to initialize ADB: ${e}`);
                error = new Error(`Unable to connect to device ${this.deviceId}: ${e}`);
            } finally{
                this.connectingAdb = null;
            }
            if (error) throw error;
            throw new Error('ADB initialization failed unexpectedly');
        })();
        return this.connectingAdb;
    }
    createAdbProxy(adb) {
        return new Proxy(adb, {
            get: (target, prop)=>{
                const originalMethod = target[prop];
                if ('function' != typeof originalMethod) return originalMethod;
                return async (...args)=>{
                    try {
                        debugDevice(`adb ${String(prop)} ${args.join(' ')}`);
                        const result = await originalMethod.apply(target, args);
                        debugDevice(`adb ${String(prop)} ${args.join(' ')} end`);
                        return result;
                    } catch (error) {
                        const methodName = String(prop);
                        const deviceId = this.deviceId;
                        const adbExecutable = target.executable.path;
                        debugDevice(`ADB error with device ${deviceId} when calling ${methodName} (ADB executable: ${adbExecutable}): ${error}`);
                        throw new Error(`ADB error with device ${deviceId} when calling ${methodName} (ADB executable: ${adbExecutable}), please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
                            cause: error
                        });
                    }
                };
            }
        });
    }
    getScrcpyStatus() {
        return this.getScrcpyAdapter().getStatus();
    }
    async retryScrcpy() {
        const adapter = this.getScrcpyAdapter();
        if (!adapter.getStatus().enabled) throw new Error('scrcpy is disabled in AndroidDevice options');
        const deviceInfo = await this.getDevicePhysicalInfo();
        await adapter.initialize(deviceInfo);
        return adapter.getStatus();
    }
    getScrcpyAdapter() {
        if (!this.scrcpyAdapter) this.scrcpyAdapter = new ScrcpyDeviceAdapter(this.deviceId, this.options?.scrcpyConfig, ()=>this.getAdb());
        return this.scrcpyAdapter;
    }
    async openScrcpyFrameSource() {
        const adapter = this.getScrcpyAdapter();
        if (!adapter.isEnabled()) throw new Error('scrcpy is not available for frame observation');
        const deviceInfo = await this.getDevicePhysicalInfo();
        const unsubscribe = await adapter.subscribeKeyframes(deviceInfo, ()=>{});
        return {
            latest: ()=>{
                const latest = adapter.getLatestRawKeyframe();
                return latest ? {
                    ref: latest,
                    capturedAt: latest.capturedAt
                } : null;
            },
            decode: async (refs)=>{
                const images = [];
                for (const frameRef of refs)images.push(await adapter.decodeRawKeyframeToJpegBase64(frameRef.ref));
                debugDevice(`frame source decoded ${images.length} raw keyframes`);
                return images;
            },
            stop: ()=>{
                unsubscribe();
            }
        };
    }
    async getDevicePhysicalInfo() {
        await this.initializeDevicePixelRatio();
        const screenSize = await this.getScreenSize();
        const sizeStr = screenSize.override || screenSize.physical;
        const match = sizeStr.match(/(\d{1,5})x(\d{1,5})/);
        if (!match) throw new Error(`Unable to parse screen size: ${sizeStr}`);
        return {
            physicalWidth: Number.parseInt(match[1], 10),
            physicalHeight: Number.parseInt(match[2], 10),
            dpr: this.devicePixelRatio,
            orientation: screenSize.orientation,
            isCurrentOrientation: screenSize.isCurrentOrientation
        };
    }
    setAppNameMapping(mapping) {
        this.appNameMapping = mapping;
    }
    resolvePackageName(appName) {
        const normalizedAppName = normalizeForComparison(appName);
        return this.appNameMapping[normalizedAppName];
    }
    async launch(uri) {
        return this.visualActions.launch(uri);
    }
    async launchRaw(uri) {
        const adb = await this.getAdb();
        this.uri = uri;
        try {
            debugDevice(`Launching app: ${uri}`);
            if (uri.startsWith('http://') || uri.startsWith('https://') || uri.includes('://')) await adb.startUri(uri);
            else if (uri.includes('/')) {
                const [appPackage, appActivity] = uri.split('/');
                await adb.startApp({
                    pkg: appPackage,
                    activity: appActivity
                });
            } else {
                const resolvedUri = this.resolvePackageName(uri) ?? uri;
                await adb.activateApp(resolvedUri);
            }
            debugDevice(`Successfully launched: ${uri}`);
        } catch (error) {
            debugDevice(`Error launching ${uri}: ${error}`);
            throw new Error(`Failed to launch ${uri}: ${error.message}`, {
                cause: error
            });
        }
        return this;
    }
    async terminate(uri) {
        await this.visualActions.terminate(uri);
    }
    async terminateRaw(uri) {
        const packagePart = uri.includes('/') ? uri.split('/')[0] : uri;
        const resolved = this.resolvePackageName(packagePart) ?? packagePart;
        const adb = await this.getAdb();
        try {
            debugDevice(`Terminating app: ${resolved}`);
            await adb.shell(`am force-stop ${resolved}`);
            debugDevice(`Successfully terminated: ${resolved}`);
        } catch (error) {
            debugDevice(`Error terminating ${resolved}: ${error}`);
            throw new Error(`Failed to terminate ${resolved}: ${error.message}`, {
                cause: error
            });
        }
    }
    async execYadb(keyboardContent) {
        await this.visualActions.execYadb(keyboardContent);
    }
    async execYadbRaw(keyboardContent) {
        this.warnYadbOnNonDefaultDisplay('keyboard input');
        await this.ensureYadb();
        const adb = await this.getAdb();
        await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
    }
    async getElementsInfo() {
        return [];
    }
    async getElementsNodeTree() {
        return {
            node: null,
            children: []
        };
    }
    async getUITree() {
        await this.initializeDevicePixelRatio();
        return captureAndroidUITree({
            adb: await this.getAdb(),
            devicePixelRatio: this.devicePixelRatio,
            displayId: this.options?.displayId,
            getDisplaySize: ()=>this.size()
        });
    }
    async getScreenSize() {
        const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
        debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
        if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
        const adb = await this.getAdb();
        if ('number' == typeof this.options?.displayId) try {
            const stdout = await adb.shell('dumpsys display');
            if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
                const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
                if (physicalDisplayId) {
                    const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
                    if (targetLine) {
                        const realMatch = targetLine.match(/real (\d+) x (\d+)/);
                        const rotationMatch = targetLine.match(/rotation (\d+)/);
                        if (realMatch && rotationMatch) {
                            const width = Number(realMatch[1]);
                            const height = Number(realMatch[2]);
                            const rotation = Number(rotationMatch[1]);
                            const sizeStr = `${width}x${height}`;
                            debugDevice(`Using display info for long ID ${physicalDisplayId}: ${sizeStr}, rotation: ${rotation}`);
                            const result = {
                                override: sizeStr,
                                physical: sizeStr,
                                orientation: rotation,
                                isCurrentOrientation: true
                            };
                            if (shouldCache) this.cachedScreenSize = result;
                            return result;
                        }
                    }
                }
            } else {
                const targetLine = displayViewportForLogicalDisplay(stdout, this.options.displayId);
                if (targetLine) {
                    const physicalFrameMatch = targetLine.match(/physicalFrame=Rect\(\d+, \d+ - (\d+), (\d+)\)/);
                    const orientationMatch = targetLine.match(/orientation=(\d+)/);
                    if (physicalFrameMatch && orientationMatch) {
                        const width = Number(physicalFrameMatch[1]);
                        const height = Number(physicalFrameMatch[2]);
                        const rotation = Number(orientationMatch[1]);
                        const sizeStr = `${width}x${height}`;
                        debugDevice(`Using display info for display ID ${this.options.displayId}: ${sizeStr}, rotation: ${rotation}`);
                        const result = {
                            override: sizeStr,
                            physical: sizeStr,
                            orientation: rotation,
                            isCurrentOrientation: true
                        };
                        if (shouldCache) this.cachedScreenSize = result;
                        return result;
                    }
                }
            }
            debugDevice(`Could not find display info for displayId ${this.options.displayId}`);
        } catch (e) {
            debugDevice(`Failed to get size from display info for display ${this.options.displayId}: ${e}`);
        }
        const stdout = await adb.shell([
            'wm',
            'size'
        ]);
        const size = {
            override: '',
            physical: ''
        };
        const overrideSize = new RegExp(/Override size: ([^\r?\n]+)*/g).exec(stdout);
        if (overrideSize && overrideSize.length >= 2 && overrideSize[1]) {
            debugDevice(`Using Override size: ${overrideSize[1].trim()}`);
            size.override = overrideSize[1].trim();
        }
        const physicalSize = new RegExp(/Physical size: ([^\r?\n]+)*/g).exec(stdout);
        if (physicalSize && physicalSize.length >= 2) {
            debugDevice(`Using Physical size: ${physicalSize[1].trim()}`);
            size.physical = physicalSize[1].trim();
        }
        const orientation = await this.getDisplayOrientation();
        if (size.override || size.physical) {
            const result = {
                ...size,
                orientation,
                isCurrentOrientation: false
            };
            if (shouldCache) this.cachedScreenSize = result;
            return result;
        }
        throw new Error(`Failed to get screen size, output: ${stdout}`);
    }
    async initializeDevicePixelRatio() {
        if (this.devicePixelRatioInitialized) return;
        const densityNum = await this.getDisplayDensity();
        this.devicePixelRatio = Number(densityNum) / 160;
        debugDevice(`Initialized device pixel ratio: ${this.devicePixelRatio}`);
        this.devicePixelRatioInitialized = true;
    }
    async getDisplayDensity() {
        const adb = await this.getAdb();
        if ('number' == typeof this.options?.displayId) try {
            const stdout = await adb.shell('dumpsys display');
            if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
                const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
                if (physicalDisplayId) {
                    const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
                    if (targetLine) {
                        const densityMatch = targetLine.match(/density (\d+)/);
                        if (densityMatch) {
                            const density = Number(densityMatch[1]);
                            debugDevice(`Using display density for physical ID ${physicalDisplayId}: ${density}`);
                            return density;
                        }
                    }
                }
            } else {
                const displayDeviceRegex = new RegExp(`DisplayDevice:[\\s\\S]*?mDisplayId=${this.options.displayId}[\\s\\S]*?DisplayInfo{[^}]*density (\\d+)`, 'm');
                const deviceBlockMatch = stdout.match(displayDeviceRegex);
                if (deviceBlockMatch) {
                    const density = Number(deviceBlockMatch[1]);
                    debugDevice(`Using display density for display ID ${this.options.displayId}: ${density}`);
                    return density;
                }
            }
        } catch (e) {
            debugDevice(`Failed to get density from display info: ${e}`);
        }
        const density = await adb.getScreenDensity();
        return density ?? 160;
    }
    async getDisplayOrientation() {
        const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
        debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
        if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
        const adb = await this.getAdb();
        let orientation = 0;
        try {
            const orientationStdout = await adb.shell(`dumpsys${this.getDisplayArg()} input | grep SurfaceOrientation`);
            const orientationMatch = orientationStdout.match(/SurfaceOrientation:\s*(\d)/);
            if (!orientationMatch) throw new Error('Failed to get orientation from input');
            orientation = Number(orientationMatch[1]);
            debugDevice(`Screen orientation: ${orientation}`);
        } catch (e) {
            debugDevice('Failed to get orientation from input, try display');
            try {
                const orientationStdout = await adb.shell(`dumpsys${this.getDisplayArg()} display | grep mCurrentOrientation`);
                const orientationMatch = orientationStdout.match(/mCurrentOrientation=(\d)/);
                if (!orientationMatch) throw new Error('Failed to get orientation from display');
                orientation = Number(orientationMatch[1]);
                debugDevice(`Screen orientation (fallback): ${orientation}`);
            } catch (e2) {
                orientation = 0;
                debugDevice('Failed to get orientation from display, default to 0');
            }
        }
        if (shouldCache) this.cachedOrientation = orientation;
        return orientation;
    }
    async getOrientedPhysicalSize() {
        const info = await this.getDevicePhysicalInfo();
        const isLandscape = 1 === info.orientation || 3 === info.orientation;
        const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
        return {
            width: shouldSwap ? info.physicalHeight : info.physicalWidth,
            height: shouldSwap ? info.physicalWidth : info.physicalHeight
        };
    }
    async size() {
        const physical = await this.getOrientedPhysicalSize();
        const scale = 1 / this.devicePixelRatio;
        return {
            width: Math.round(physical.width * scale),
            height: Math.round(physical.height * scale)
        };
    }
    async getAdjustScale() {
        const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
        debugDevice(`getAdjustScale: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedScale=${!!this.cachedAdjustScale}`);
        if (shouldCache && this.cachedAdjustScale) return this.cachedAdjustScale;
        const physical = await this.getOrientedPhysicalSize();
        const { width: logicalW, height: logicalH } = await this.size();
        const scale = {
            x: logicalW / physical.width,
            y: logicalH / physical.height
        };
        if (shouldCache) this.cachedAdjustScale = scale;
        return scale;
    }
    async adjustCoordinates(x, y) {
        const scale = await this.getAdjustScale();
        return {
            x: Math.round(x / scale.x),
            y: Math.round(y / scale.y)
        };
    }
    calculateScrollEndPoint(start, deltaX, deltaY, maxWidth, maxHeight) {
        const minScrollDistance = 50;
        let actualScrollDistanceX = 0;
        let actualScrollDistanceY = 0;
        if (0 !== deltaX) {
            const maxAvailableX = deltaX > 0 ? maxWidth - start.x : start.x;
            actualScrollDistanceX = Math.min(Math.abs(deltaX), maxAvailableX);
            const minScrollX = Math.min(minScrollDistance, actualScrollDistanceX);
            actualScrollDistanceX = Math.max(minScrollX, actualScrollDistanceX);
        }
        if (0 !== deltaY) {
            const maxAvailableY = deltaY > 0 ? maxHeight - start.y : start.y;
            actualScrollDistanceY = Math.min(Math.abs(deltaY), maxAvailableY);
            const minScrollY = Math.min(minScrollDistance, actualScrollDistanceY);
            actualScrollDistanceY = Math.max(minScrollY, actualScrollDistanceY);
        }
        const endX = Math.round(0 === deltaX ? start.x : deltaX > 0 ? Math.min(maxWidth, start.x + actualScrollDistanceX) : Math.max(0, start.x - actualScrollDistanceX));
        const endY = Math.round(0 === deltaY ? start.y : deltaY > 0 ? Math.min(maxHeight, start.y + actualScrollDistanceY) : Math.max(0, start.y - actualScrollDistanceY));
        return {
            x: endX,
            y: endY
        };
    }
    warnScrollDistanceClamped(direction, requestedDistance, appliedDistance) {
        if (requestedDistance <= appliedDistance) return;
        const scrollToSuggestion = {
            down: 'scrollToBottom',
            up: 'scrollToTop',
            left: 'scrollToLeft',
            right: 'scrollToRight'
        };
        const edgeLabel = {
            down: 'bottom',
            up: 'top',
            left: 'left edge',
            right: 'right edge'
        };
        warnDevice(`[midscene] Android ADB swipe coordinates must stay within the screen bounds. The requested scroll distance (${requestedDistance}px) exceeds the maximum single swipe distance (${appliedDistance}px) from the current start point, so it will be clamped. If you want to scroll to the ${edgeLabel[direction]}, use ${scrollToSuggestion[direction]} instead.`);
    }
    async screenshotBase64() {
        debugDevice('screenshotBase64 begin');
        const adapter = this.getScrcpyAdapter();
        const screenshotStrategy = this.options?.screenshotStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_SCREENSHOT_STRATEGY) || SCREENSHOT_STRATEGY_AUTO;
        if (screenshotStrategy === SCREENSHOT_STRATEGY_ALWAYS_YADB) return this.prepareFallbackScreenshot(await this.screenshotBase64ViaYadb());
        if (adapter.isEnabled()) try {
            debugDevice('Attempting scrcpy screenshot...');
            const scrcpyDeviceInfo = await this.getDevicePhysicalInfo();
            const result = await adapter.screenshotBase64(scrcpyDeviceInfo);
            debugDevice('screenshotBase64 end (scrcpy mode)');
            return result;
        } catch (error) {
            const diagnostic = (0, scrcpy_manager.GJ)(error) ? `\n${formatScrcpyFreshFrameFailure(error)}` : '';
            warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.${diagnostic}\nError: ${error}`);
        }
        const adb = await this.getAdb();
        let screenshotBuffer;
        const useShellScreencap = 'number' == typeof this.options?.displayId;
        try {
            if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
                debugDevice('Taking screenshot via adb.takeScreenshot');
                screenshotBuffer = await adb.takeScreenshot.call(adb);
                debugDevice('adb.takeScreenshot completed');
                try {
                    (0, img_.validateScreenshotBuffer)(screenshotBuffer, {
                        label: 'Screenshot',
                        minBufferSize: this.options?.minScreenshotBufferSize ?? AndroidDevice.DEFAULT_MIN_SCREENSHOT_BUFFER_SIZE
                    });
                } catch (validationError) {
                    debugDevice('Invalid screenshot buffer detected: %s', validationError instanceof Error ? validationError.message : String(validationError));
                    this.takeScreenshotFailCount++;
                    throw validationError;
                }
                this.takeScreenshotFailCount = 0;
            } else {
                if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
                throw new Error('Using shell screencap directly');
            }
        } catch (error) {
            debugDevice(`Taking screenshot via adb.takeScreenshot failed or was skipped: ${error}`);
            const result = await this.captureScreenshotBase64FromDeviceFile('Fallback screenshot', async (androidScreenshotPath)=>{
                debugDevice('Fallback: taking screenshot via shell screencap');
                const displayId = this.options?.usePhysicalDisplayIdForScreenshot ? await this.getPhysicalDisplayId() : this.options?.displayId;
                const displayArg = displayId ? `-d ${displayId}` : '';
                try {
                    await adb.shell(`screencap -p ${displayArg} ${androidScreenshotPath}`.trim());
                    debugDevice('adb.shell screencap completed');
                } catch (screencapError) {
                    debugDevice('screencap failed, using forceScreenshot');
                    await this.forceScreenshot(androidScreenshotPath);
                    debugDevice('forceScreenshot completed');
                }
            });
            debugDevice('screenshotBase64 end (fallback)');
            return this.prepareFallbackScreenshot(result);
        }
        if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
        debugDevice('Converting to base64');
        const result = (0, img_.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
        debugDevice('screenshotBase64 end');
        return this.prepareFallbackScreenshot(result);
    }
    async prepareFallbackScreenshot(screenshotBase64) {
        const adapter = this.getScrcpyAdapter();
        if (!adapter.getStatus().enabled) return screenshotBase64;
        const { maxSize } = adapter.resolveConfig();
        if (0 === maxSize) return screenshotBase64;
        return (0, img_.constrainBase64ImageToMaxSize)(screenshotBase64, {
            maxSize
        });
    }
    async captureScreenshotBase64FromDeviceFile(label, capture) {
        const adb = await this.getAdb();
        const screenshotId = Date.now().toString(36);
        const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
        const localScreenshotPath = getTmpFile('png');
        try {
            await capture(androidScreenshotPath);
            debugDevice('Pulling screenshot file from device');
            await adb.pull(androidScreenshotPath, localScreenshotPath);
            debugDevice(`adb.pull completed, local path: ${localScreenshotPath}`);
            const screenshotBuffer = await external_node_fs_["default"].promises.readFile(localScreenshotPath);
            (0, img_.validateScreenshotBuffer)(screenshotBuffer, {
                label,
                minBufferSize: this.options?.minScreenshotBufferSize ?? AndroidDevice.DEFAULT_MIN_SCREENSHOT_BUFFER_SIZE
            });
            debugDevice(`${label} validated successfully: ${screenshotBuffer.length} bytes`);
            return (0, img_.createImgBase64ByFormat)('png', screenshotBuffer.toString('base64'));
        } finally{
            const adbPath = adb.executable?.path ?? 'adb';
            const adbDefaultArgs = adb.executable?.defaultArgs ?? [];
            const child = execFile(adbPath, [
                ...adbDefaultArgs,
                '-s',
                this.deviceId,
                'shell',
                `rm ${androidScreenshotPath}`
            ], {
                timeout: 3000
            }, (error)=>{
                if (error) debugDevice('Failed to delete remote screenshot: %s', error.message);
            });
            child.unref();
            (0, external_node_fs_.unlink)(localScreenshotPath, (error)=>{
                if (error && 'ENOENT' !== error.code) debugDevice('Failed to delete local screenshot: %s', error.message);
            });
        }
    }
    async screenshotBase64ViaYadb() {
        debugDevice('screenshotBase64ViaYadb begin');
        if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`screenshotStrategy 'always-yadb' cannot target non-default displayId=${this.options.displayId}. Use displayId=0 or screenshotStrategy 'auto'.`);
        const result = await this.captureScreenshotBase64FromDeviceFile('Yadb screenshot', async (androidScreenshotPath)=>{
            debugDevice('Taking screenshot via yadb (always-yadb strategy)');
            await this.forceScreenshot(androidScreenshotPath);
            debugDevice('forceScreenshot completed');
        });
        debugDevice('screenshotBase64ViaYadb end');
        return result;
    }
    async clearInput(element) {
        await this.visualActions.clearInput(element);
    }
    async clearInputRaw(element) {
        if (element) await this.tapPoint({
            x: element.center[0],
            y: element.center[1]
        });
        const adb = await this.getAdb();
        await this.clearInputWithKeyboard(adb);
        if (await adb.isSoftKeyboardPresent()) return;
        if (element) await this.tapPoint({
            x: element.center[0],
            y: element.center[1]
        });
    }
    async clearInputWithKeyboard(adb) {
        const isNonDefaultDisplay = 'number' == typeof this.options?.displayId && 0 !== this.options.displayId;
        const imeStrategy = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
        if (!isNonDefaultDisplay && imeStrategy === IME_STRATEGY_ALWAYS_YADB) {
            await this.ensureYadb();
            await adb.shell('app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboardClear');
            return;
        }
        const keys = [
            androidClearInputKeyCodes.moveEnd
        ];
        for(let index = 0; index < androidTextClearKeyRepeatCount; index++)keys.push(androidClearInputKeyCodes.backwardDelete, androidClearInputKeyCodes.forwardDelete);
        await this.shellInputKeyevent(...keys);
    }
    async forceScreenshot(path) {
        await this.ensureYadb();
        const adb = await this.getAdb();
        await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -screenshot ${path}`);
    }
    async url() {
        return '';
    }
    async scrollUntilTop(startPoint) {
        await this.visualActions.scrollUntilTop(startPoint);
    }
    async scrollUntilTopRaw(startPoint) {
        if (startPoint) {
            const { height } = await this.size();
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = {
                x: start.x,
                y: Math.round(height)
            };
            await repeat(defaultScrollUntilTimes, ()=>this.dragPoint(start, end, defaultFastScrollDuration));
            await sleep(1000);
            return;
        }
        await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, -9999999, defaultFastScrollDuration));
        await sleep(1000);
    }
    async scrollUntilBottom(startPoint) {
        await this.visualActions.scrollUntilBottom(startPoint);
    }
    async scrollUntilBottomRaw(startPoint) {
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = {
                x: start.x,
                y: 0
            };
            await repeat(defaultScrollUntilTimes, ()=>this.dragPoint(start, end, defaultFastScrollDuration));
            await sleep(1000);
            return;
        }
        await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(0, 9999999, defaultFastScrollDuration));
        await sleep(1000);
    }
    async scrollUntilLeft(startPoint) {
        await this.visualActions.scrollUntilLeft(startPoint);
    }
    async scrollUntilLeftRaw(startPoint) {
        if (startPoint) {
            const { width } = await this.size();
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = {
                x: Math.round(width),
                y: start.y
            };
            await repeat(defaultScrollUntilTimes, ()=>this.dragPoint(start, end, defaultFastScrollDuration));
            await sleep(1000);
            return;
        }
        await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(-9999999, 0, defaultFastScrollDuration));
        await sleep(1000);
    }
    async scrollUntilRight(startPoint) {
        await this.visualActions.scrollUntilRight(startPoint);
    }
    async scrollUntilRightRaw(startPoint) {
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = {
                x: 0,
                y: start.y
            };
            await repeat(defaultScrollUntilTimes, ()=>this.dragPoint(start, end, defaultFastScrollDuration));
            await sleep(1000);
            return;
        }
        await repeat(defaultScrollUntilTimes, ()=>this.scrollRaw(9999999, 0, defaultFastScrollDuration));
        await sleep(1000);
    }
    async scrollUp(distance, startPoint) {
        await this.visualActions.scrollUp(distance, startPoint);
    }
    async scrollUpRaw(distance, startPoint) {
        const { height } = await this.size();
        const scrollDistance = Math.round(distance || height);
        const hasExplicitDistance = void 0 !== distance;
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = this.calculateScrollEndPoint(start, 0, scrollDistance, 0, height);
            if (hasExplicitDistance) this.warnScrollDistanceClamped('up', scrollDistance, Math.abs(end.y - start.y));
            await this.dragPoint(start, end);
            return;
        }
        await this.scrollRaw(0, -scrollDistance, void 0, hasExplicitDistance, 'up');
    }
    async scrollDown(distance, startPoint) {
        await this.visualActions.scrollDown(distance, startPoint);
    }
    async scrollDownRaw(distance, startPoint) {
        const { height } = await this.size();
        const scrollDistance = Math.round(distance || height);
        const hasExplicitDistance = void 0 !== distance;
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = this.calculateScrollEndPoint(start, 0, -scrollDistance, 0, height);
            if (hasExplicitDistance) this.warnScrollDistanceClamped('down', scrollDistance, Math.abs(end.y - start.y));
            await this.dragPoint(start, end);
            return;
        }
        await this.scrollRaw(0, scrollDistance, void 0, hasExplicitDistance, 'down');
    }
    async scrollLeft(distance, startPoint) {
        await this.visualActions.scrollLeft(distance, startPoint);
    }
    async scrollLeftRaw(distance, startPoint) {
        const { width } = await this.size();
        const scrollDistance = Math.round(distance || width);
        const hasExplicitDistance = void 0 !== distance;
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = this.calculateScrollEndPoint(start, scrollDistance, 0, width, 0);
            if (hasExplicitDistance) this.warnScrollDistanceClamped('left', scrollDistance, Math.abs(end.x - start.x));
            await this.dragPoint(start, end);
            return;
        }
        await this.scrollRaw(-scrollDistance, 0, void 0, hasExplicitDistance, 'left');
    }
    async scrollRight(distance, startPoint) {
        await this.visualActions.scrollRight(distance, startPoint);
    }
    async scrollRightRaw(distance, startPoint) {
        const { width } = await this.size();
        const scrollDistance = Math.round(distance || width);
        const hasExplicitDistance = void 0 !== distance;
        if (startPoint) {
            const start = {
                x: Math.round(startPoint.left),
                y: Math.round(startPoint.top)
            };
            const end = this.calculateScrollEndPoint(start, -scrollDistance, 0, width, 0);
            if (hasExplicitDistance) this.warnScrollDistanceClamped('right', scrollDistance, Math.abs(end.x - start.x));
            await this.dragPoint(start, end);
            return;
        }
        await this.scrollRaw(scrollDistance, 0, void 0, hasExplicitDistance, 'right');
    }
    async ensureYadb() {
        if (!this.yadbPushed) {
            const adb = await this.getAdb();
            const yadbBin = this.resolveYadbBinPath();
            await adb.push(yadbBin, '/data/local/tmp');
            this.yadbPushed = true;
        }
    }
    resolveYadbBinPath() {
        const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
        return (0, resource_path.J)(external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb'));
    }
    shouldUseYadbForText(text) {
        const hasNonAscii = /[\x80-\uFFFF]/.test(text);
        const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
        const hasShellSpecialChars = /[\\`$]/.test(text);
        const hasBothQuotes = text.includes('"') && text.includes("'");
        return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
    }
    async typeText(text, options, resolvedInputOptions = resolveTextInputOptions(options, this.options)) {
        if (!text) return;
        const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
        const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
        const { inputStrategy, keyboardTypeDelay: typeDelay } = resolvedInputOptions;
        const inputYadbSequentially = 'sequential' === inputStrategy;
        const isNonDefaultDisplay = 'number' == typeof this.options?.displayId && 0 !== this.options.displayId;
        const needsYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
        if (isNonDefaultDisplay && needsYadb) {
            const reason = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB ? "imeStrategy 'always-yadb' requires yadb" : 'text contains characters that require yadb (non-ASCII, format specifiers, or mixed quotes)';
            throw new Error(`${reason}, but yadb (app_process) cannot target non-default displayId=${this.options?.displayId}. Use displayId=0 or a different imeStrategy.`);
        }
        if (needsYadb) if (inputYadbSequentially) await sendTextSequentially(text, {
            sendCharacter: (character)=>this.execYadbRaw(escapeForShell(character)),
            wait: sleep
        }, {
            delayMs: typeDelay
        });
        else await this.execYadbRaw(escapeForShell(text));
        else await this.shellInputText(text, resolvedInputOptions);
        if (true === shouldAutoDismissKeyboard) await this.hideKeyboardRaw(options);
    }
    normalizeKeyName(key) {
        const keyMap = {
            enter: 'Enter',
            backspace: 'Backspace',
            tab: 'Tab',
            escape: 'Escape',
            esc: 'Escape',
            home: 'Home',
            end: 'End',
            arrowup: 'ArrowUp',
            arrowdown: 'ArrowDown',
            arrowleft: 'ArrowLeft',
            arrowright: 'ArrowRight',
            up: 'ArrowUp',
            down: 'ArrowDown',
            left: 'ArrowLeft',
            right: 'ArrowRight'
        };
        const lowerKey = key.toLowerCase();
        return keyMap[lowerKey] || key;
    }
    async pressKey(key) {
        if ('+' !== key.trim() && key.includes('+')) throw new Error(`Android keyboardPress does not support key combinations: ${JSON.stringify(key)}`);
        const keyCodeMap = {
            Enter: 66,
            Backspace: 67,
            Tab: 61,
            ArrowUp: 19,
            ArrowDown: 20,
            ArrowLeft: 21,
            ArrowRight: 22,
            Escape: 111,
            Home: 3,
            End: 123
        };
        const normalizedKey = this.normalizeKeyName(key);
        const keyCode = keyCodeMap[normalizedKey];
        if (void 0 !== keyCode) await this.shellInputKeyevent(keyCode);
        else if (1 === key.length) {
            const asciiCode = key.toUpperCase().charCodeAt(0);
            if (asciiCode >= 65 && asciiCode <= 90) await this.shellInputKeyevent(asciiCode - 36);
        }
    }
    async tapPoint(point) {
        const adb = await this.getAdb();
        const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(point.x, point.y);
        await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
    }
    async doubleTapPoint(point) {
        const adb = await this.getAdb();
        const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(point.x, point.y);
        const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
        await adb.shell(tapCommand);
        await sleep(50);
        await adb.shell(tapCommand);
    }
    async mouseMove() {
        return Promise.resolve();
    }
    async dragPoint(from, to, duration) {
        await this.swipePoint(from, to, duration ?? defaultNormalScrollDuration);
    }
    async swipePoint(from, to, duration) {
        const adb = await this.getAdb();
        const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
        const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
        await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
    }
    async scroll(deltaX, deltaY, duration, warnOnClamp = false, direction) {
        await this.visualActions.scroll(deltaX, deltaY, duration, warnOnClamp, direction);
    }
    async scrollRaw(deltaX, deltaY, duration, warnOnClamp = false, direction) {
        if (0 === deltaX && 0 === deltaY) throw new Error('Scroll distance cannot be zero in both directions');
        const { width, height } = await this.size();
        const n = 4;
        const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
        const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
        const maxPositiveDeltaX = startX;
        const maxNegativeDeltaX = width - startX;
        const maxPositiveDeltaY = startY;
        const maxNegativeDeltaY = height - startY;
        const originalDeltaX = deltaX;
        const originalDeltaY = deltaY;
        deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
        deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
        if (warnOnClamp && direction && (deltaX !== originalDeltaX || deltaY !== originalDeltaY)) {
            const requestedDistance = 'left' === direction || 'right' === direction ? Math.abs(originalDeltaX) : Math.abs(originalDeltaY);
            const appliedDistance = 'left' === direction || 'right' === direction ? Math.abs(deltaX) : Math.abs(deltaY);
            this.warnScrollDistanceClamped(direction, requestedDistance, appliedDistance);
        }
        const endX = Math.round(startX - deltaX);
        const endY = Math.round(startY - deltaY);
        const swipeDuration = duration ?? defaultNormalScrollDuration;
        await this.swipePoint({
            x: startX,
            y: startY
        }, {
            x: endX,
            y: endY
        }, swipeDuration);
    }
    async destroy() {
        if (this.destroyed) return;
        this.destroyed = true;
        this.cachedPhysicalDisplayId = void 0;
        this.cachedScreenSize = null;
        this.cachedOrientation = null;
        this.cachedAdjustScale = null;
        if (this.scrcpyAdapter) {
            await this.scrcpyAdapter.disconnect();
            this.scrcpyAdapter = null;
        }
        try {
            if (this.adb) this.adb = null;
        } catch (error) {
            console.error('Error during cleanup:', error);
        }
        this.connectingAdb = null;
        this.yadbPushed = false;
    }
    async getDeviceLocalTimeString(format = 'YYYY-MM-DD HH:mm:ss') {
        const adb = await this.getAdb();
        try {
            const stdout = await adb.shell('date +%Y-%m-%dT%H:%M:%S');
            const match = stdout.trim().match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/);
            if (!match) throw new Error(`Invalid device time format: ${stdout}`);
            const [, year, month, day, hours, minutes, seconds] = match;
            const timeString = format.replace('YYYY', year).replace('MM', month).replace('DD', day).replace('HH', hours).replace('mm', minutes).replace('ss', seconds);
            debugDevice(`Got device local time: ${timeString}`);
            return `${timeString} (${format})`;
        } catch (error) {
            debugDevice(`Failed to get device local time: ${error}`);
            throw new Error(`Failed to get device local time: ${error}`);
        }
    }
    async back() {
        await this.visualActions.back();
    }
    async backRaw() {
        await this.shellInputKeyevent(4);
    }
    async home() {
        await this.visualActions.home();
    }
    async homeRaw() {
        await this.shellInputKeyevent(3);
    }
    async recentApps() {
        await this.visualActions.recentApps();
    }
    async recentAppsRaw() {
        await this.shellInputKeyevent(187);
    }
    async longPressPoint(point, duration = 2000) {
        const adb = await this.getAdb();
        const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(point.x, point.y);
        await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
    }
    async pullDown(startPoint, distance, duration = 800) {
        await this.visualActions.pullDown(startPoint, distance, duration);
    }
    async pullDownRaw(startPoint, distance, duration = 800) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(0.15 * height)
        };
        const pullDistance = Math.round(distance || 0.5 * height);
        const end = {
            x: start.x,
            y: start.y + pullDistance
        };
        await this.pullDragRaw(start, end, duration);
        await sleep(200);
    }
    async pullDrag(from, to, duration) {
        await this.visualActions.pullDrag(from, to, duration);
    }
    async pullDragRaw(from, to, duration) {
        await this.swipePoint(from, to, duration);
    }
    async pullUp(startPoint, distance, duration = 600) {
        await this.visualActions.pullUp(startPoint, distance, duration);
    }
    async pullUpRaw(startPoint, distance, duration = 600) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(0.85 * height)
        };
        const pullDistance = Math.round(distance || 0.4 * height);
        const end = {
            x: start.x,
            y: start.y - pullDistance
        };
        await this.pullDragRaw(start, end, duration);
        await sleep(100);
    }
    getDisplayArg() {
        return 'number' == typeof this.options?.displayId ? ` -d ${this.options.displayId}` : '';
    }
    async shellInputKeyevent(...keyCodes) {
        const adb = await this.getAdb();
        await adb.shell(`input${this.getDisplayArg()} keyevent ${keyCodes.join(' ')}`);
    }
    async shellInputText(text, inputOptions) {
        const adb = await this.getAdb();
        const displayArg = this.getDisplayArg();
        const typeDelay = inputOptions.keyboardTypeDelay;
        const inputSequentially = shouldInputSequentially(inputOptions);
        const segments = text.split('\n');
        for(let i = 0; i < segments.length; i++){
            if (segments[i].length > 0) if (inputSequentially) await sendTextSequentially(segments[i], {
                sendCharacter: (character)=>adb.shell(`input${displayArg} text ${shellEscapeArg(character)}`),
                wait: sleep
            }, {
                delayMs: typeDelay,
                delayAfterLast: 'legacy' === inputOptions.inputStrategy
            });
            else await adb.shell(`input${displayArg} text ${shellEscapeArg(segments[i])}`);
            if (i < segments.length - 1) await this.shellInputKeyevent(66);
        }
    }
    warnYadbOnNonDefaultDisplay(operation) {
        if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) warnDevice(`yadb ${operation} cannot target display ${this.options.displayId}; it will act on the default display.`);
    }
    async getPhysicalDisplayId() {
        return this.resolvePhysicalDisplayId();
    }
    async resolvePhysicalDisplayId(displayDump) {
        if (void 0 !== this.cachedPhysicalDisplayId) return this.cachedPhysicalDisplayId;
        if ('number' != typeof this.options?.displayId) {
            this.cachedPhysicalDisplayId = null;
            return null;
        }
        const adb = await this.getAdb();
        try {
            const resolvedDisplayDump = displayDump ?? await adb.shell('dumpsys display');
            const logicalDisplayPhysicalId = physicalDisplayIdForLogicalDisplay(resolvedDisplayDump, this.options.displayId);
            if (logicalDisplayPhysicalId) {
                this.cachedPhysicalDisplayId = logicalDisplayPhysicalId;
                debugDevice(`Found and cached physical display ID: ${logicalDisplayPhysicalId} for logical display ID: ${this.options.displayId}`);
                return this.cachedPhysicalDisplayId;
            }
            const stdout = await adb.shell(`dumpsys SurfaceFlinger --display-id ${this.options.displayId}`);
            const regex = new RegExp(`Display (\\d+) \\(HWC display ${this.options.displayId}\\):`);
            const displayMatch = stdout.match(regex);
            if (displayMatch?.[1]) {
                this.cachedPhysicalDisplayId = displayMatch[1];
                debugDevice(`Found and cached physical display ID: ${displayMatch[1]} for display ID: ${this.options.displayId}`);
                return this.cachedPhysicalDisplayId;
            }
            this.cachedPhysicalDisplayId = null;
            debugDevice(`Could not find physical display ID for display ID: ${this.options.displayId}`);
            return null;
        } catch (error) {
            debugDevice(`Error getting physical display ID: ${error}`);
            this.cachedPhysicalDisplayId = null;
            return null;
        }
    }
    async hideKeyboard(options, timeoutMs = 1000) {
        return this.visualActions.hideKeyboard(options, timeoutMs);
    }
    async hideKeyboardRaw(options, timeoutMs = 1000) {
        const adb = await this.getAdb();
        const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
        const keyboardStatus = await adb.isSoftKeyboardPresent();
        const isKeyboardShown = 'boolean' == typeof keyboardStatus ? keyboardStatus : keyboardStatus?.isKeyboardShown;
        if (!isKeyboardShown) {
            debugDevice('Keyboard has no UI; no closing necessary');
            return false;
        }
        const keyCodes = 'back-first' === keyboardDismissStrategy ? [
            4,
            111
        ] : [
            111,
            4
        ];
        for (const keyCode of keyCodes){
            await this.shellInputKeyevent(keyCode);
            const startTime = Date.now();
            const intervalMs = 100;
            while(Date.now() - startTime < timeoutMs){
                await sleep(intervalMs);
                const currentStatus = await adb.isSoftKeyboardPresent();
                const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
                if (!isStillShown) {
                    debugDevice(`Keyboard hidden successfully with keycode ${keyCode}`);
                    return true;
                }
            }
            debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
        }
        warnDevice('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
        return false;
    }
    constructor(deviceId, options){
        device_define_property(this, "deviceId", void 0);
        device_define_property(this, "yadbPushed", false);
        device_define_property(this, "devicePixelRatio", 1);
        device_define_property(this, "devicePixelRatioInitialized", false);
        device_define_property(this, "adb", null);
        device_define_property(this, "connectingAdb", null);
        device_define_property(this, "destroyed", false);
        device_define_property(this, "description", void 0);
        device_define_property(this, "customActions", void 0);
        device_define_property(this, "cachedScreenSize", null);
        device_define_property(this, "cachedOrientation", null);
        device_define_property(this, "cachedPhysicalDisplayId", void 0);
        device_define_property(this, "scrcpyAdapter", null);
        device_define_property(this, "openFrameSource", void 0);
        device_define_property(this, "appNameMapping", {});
        device_define_property(this, "cachedAdjustScale", null);
        device_define_property(this, "takeScreenshotFailCount", 0);
        device_define_property(this, "interfaceType", 'android');
        device_define_property(this, "uri", void 0);
        device_define_property(this, "options", void 0);
        device_define_property(this, "visualActions", createVisualActionRegistry({
            tap: (point)=>this.tapPoint(point),
            doubleClick: (point)=>this.doubleTapPoint(point),
            longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
            dragAndDrop: (from, to)=>this.dragPoint(from, to),
            keyboardPress: (keyName)=>this.pressKey(keyName),
            typeText: async (value, opts)=>{
                const resolvedInputOptions = resolveTextInputOptions(opts, this.options);
                const target = opts?.target;
                if (target && opts?.replace !== false) await this.clearInputRaw(target);
                else if (target) await this.tapPoint({
                    x: target.center[0],
                    y: target.center[1]
                });
                if (opts?.focusOnly) return;
                await this.typeText(value, opts, resolvedInputOptions);
            },
            clearInput: (target)=>this.clearInputRaw(target),
            cursorMove: async (direction, times = 1)=>{
                const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
                for(let index = 0; index < times; index++)await this.pressKey(arrowKey);
            },
            swipe: async (start, end, opts)=>{
                const duration = opts?.duration ?? 300;
                const repeatCount = opts?.repeat ?? 1;
                for(let index = 0; index < repeatCount; index++)await this.dragPoint(start, end, duration);
            },
            pinch: async (center, opts)=>{
                if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) throw new Error(`Pinch is not supported on a non-default display (displayId=${this.options.displayId}). The underlying yadb tool only injects gestures into the default display.`);
                const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(Math.round(center.x), Math.round(center.y));
                const ratio = 0 !== adjCenterX && 0 !== center.x ? adjCenterX / center.x : 1;
                const adjStartDist = Math.round(opts.startDistance * ratio);
                const adjEndDist = Math.round(opts.endDistance * ratio);
                await this.ensureYadb();
                const adb = await this.getAdb();
                await adb.shell(`app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${opts.duration}`);
            },
            actionScroll: (param)=>this.performActionScroll(param),
            back: ()=>this.backRaw(),
            home: ()=>this.homeRaw(),
            recentApps: ()=>this.recentAppsRaw(),
            pullGesture: (param)=>this.performPullGesture(param),
            launch: (uri)=>this.launchRaw(uri),
            terminate: (uri)=>this.terminateRaw(uri),
            runAdbShell: (param, context)=>this.runAdbShellRaw(param, context),
            execYadb: (keyboardContent)=>this.execYadbRaw(keyboardContent),
            scrollUntilTop: (startPoint)=>this.scrollUntilTopRaw(startPoint),
            scrollUntilBottom: (startPoint)=>this.scrollUntilBottomRaw(startPoint),
            scrollUntilLeft: (startPoint)=>this.scrollUntilLeftRaw(startPoint),
            scrollUntilRight: (startPoint)=>this.scrollUntilRightRaw(startPoint),
            scrollUp: (distance, startPoint)=>this.scrollUpRaw(distance, startPoint),
            scrollDown: (distance, startPoint)=>this.scrollDownRaw(distance, startPoint),
            scrollLeft: (distance, startPoint)=>this.scrollLeftRaw(distance, startPoint),
            scrollRight: (distance, startPoint)=>this.scrollRightRaw(distance, startPoint),
            scroll: (deltaX, deltaY, duration, warnOnClamp, direction)=>this.scrollRaw(deltaX, deltaY, duration, warnOnClamp, direction),
            pullDown: (startPoint, distance, duration)=>this.pullDownRaw(startPoint, distance, duration),
            pullDrag: (from, to, duration)=>this.pullDragRaw(from, to, duration),
            pullUp: (startPoint, distance, duration)=>this.pullUpRaw(startPoint, distance, duration),
            hideKeyboard: (options, timeoutMs)=>this.hideKeyboardRaw(options, timeoutMs)
        }, async ()=>{
            await this.scrcpyAdapter?.markActionBarrier();
        }));
        device_define_property(this, "inputPrimitives", {
            pointer: {
                tap: this.visualActions.tap,
                doubleClick: this.visualActions.doubleClick,
                longPress: this.visualActions.longPress,
                dragAndDrop: this.visualActions.dragAndDrop
            },
            keyboard: {
                keyboardPress: this.visualActions.keyboardPress,
                typeText: this.visualActions.typeText,
                clearInput: this.visualActions.clearInput,
                cursorMove: this.visualActions.cursorMove
            },
            touch: {
                swipe: this.visualActions.swipe,
                pinch: this.visualActions.pinch
            },
            scroll: {
                scroll: this.visualActions.actionScroll
            },
            system: {
                backButton: this.visualActions.back,
                homeButton: this.visualActions.home,
                recentAppsButton: this.visualActions.recentApps
            }
        });
        node_assert(deviceId, 'deviceId is required for AndroidDevice');
        this.deviceId = deviceId;
        this.options = options;
        this.customActions = options?.customActions;
        if (options?.scrcpyConfig?.enabled) this.openFrameSource = ()=>this.openScrcpyFrameSource();
    }
}
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
device_define_property(AndroidDevice, "DEFAULT_MIN_SCREENSHOT_BUFFER_SIZE", 1024);
const runAdbShellParamSchema = z.object({
    command: z.string().describe('ADB shell command to execute'),
    timeout: z.number().optional().describe('ADB shell command execution timeout in milliseconds. Only include this parameter when the user explicitly requests a timeout; otherwise, omit it.')
});
const launchParamSchema = z.object({
    uri: z.string().describe('App name, package name, or URL to launch. Prioritize using the exact package name or URL the user has provided. If none provided, use the accurate app name.')
});
const terminateParamSchema = z.object({
    uri: z.string().describe('Package name or app name to terminate. Use the exact package name, e.g. com.android.settings.')
});
const createPlatformActions = (visualActions)=>({
        RunAdbShell: defineAction({
            name: 'RunAdbShell',
            description: 'Execute an ADB shell command on the Android device and return the command stdout. Read the returned stdout to decide the next step; the stdout may indicate either success or failure.',
            interfaceAlias: 'runAdbShell',
            paramSchema: runAdbShellParamSchema,
            sample: {
                command: 'dumpsys window displays | grep -E "mCurrentFocus"'
            },
            call: async (param, context)=>{
                if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
                return visualActions.runAdbShell(param, context);
            }
        }),
        Launch: defineAction({
            name: 'Launch',
            description: 'Launch an Android app or URL',
            interfaceAlias: 'launch',
            paramSchema: launchParamSchema,
            sample: {
                uri: 'com.example.app'
            },
            call: async (param)=>{
                if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
                await visualActions.launch(param.uri);
            }
        }),
        Terminate: defineAction({
            name: 'Terminate',
            description: 'Terminate (force-stop) an Android app by package name',
            interfaceAlias: 'terminate',
            paramSchema: terminateParamSchema,
            call: async (param)=>{
                if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
                await visualActions.terminate(param.uri);
            }
        })
    });
const debugUtils = (0, logger_.getDebug)('android:utils');
async function getConnectedDevices(deviceOptions) {
    let adbExecutable;
    try {
        const adb = await adb_createAndroidAdb({
            adbExecTimeout: 60000,
            deviceOptions
        });
        adbExecutable = adb.executable.path;
        const devices = await adb.getConnectedDevices();
        debugUtils(`Found ${devices.length} connected devices: `, devices);
        return devices;
    } catch (error) {
        console.error('Failed to get device list:', error);
        const adbExecutableContext = adbExecutable ? ` (ADB executable: ${adbExecutable})` : '';
        throw new Error(`Unable to get connected Android device list${adbExecutableContext}, please check https://midscenejs.com/integrate-with-android.html#faq : ${error.message}`, {
            cause: error
        });
    }
}
function agent_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debugAgent = (0, logger_.getDebug)('android:agent');
class AndroidAgent extends Agent {
    async launch(uri) {
        const action = this.wrapActionInActionSpace('Launch');
        return action({
            uri
        });
    }
    async terminate(uri) {
        const action = this.wrapActionInActionSpace('Terminate');
        return action({
            uri
        });
    }
    async runAdbShell(command, opt) {
        const action = this.wrapActionInActionSpace('RunAdbShell');
        return action({
            command,
            ...opt
        });
    }
    createActionWrapper(name) {
        const action = this.wrapActionInActionSpace(name);
        return (...args)=>action(args[0]);
    }
    constructor(device, opts){
        super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0);
        this.appNameMapping = mergeAndNormalizeAppNameMapping(defaultAppNameMapping, opts?.appNameMapping);
        device.setAppNameMapping(this.appNameMapping);
        this.back = this.createActionWrapper('AndroidBackButton');
        this.home = this.createActionWrapper('AndroidHomeButton');
        this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');
    }
}
async function agentFromAdbDevice(deviceId, opts) {
    if (!deviceId) {
        const devices = await getConnectedDevices(opts);
        if (0 === devices.length) throw new Error('No Android devices found. Please connect an Android device and ensure ADB is properly configured. Run `adb devices` to verify device connection.');
        deviceId = devices[0].udid;
        debugAgent('deviceId not specified, will use the first device (id = %s)', deviceId);
    }
    const device = new AndroidDevice(deviceId, opts || {});
    await device.connect();
    return new AndroidAgent(device, opts);
}
function agent_tools_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debug = (0, logger_.getDebug)('agent-tools:android');
function adaptAndroidInitArgs(extracted) {
    if (!extracted) return;
    const scrcpyVideoBitRate = 'number' == typeof extracted.scrcpyVideoBitRate ? extracted.scrcpyVideoBitRate : void 0;
    const requestedUseScrcpy = 'boolean' == typeof extracted.useScrcpy ? extracted.useScrcpy : void 0;
    const useScrcpy = void 0 !== scrcpyVideoBitRate ? true : requestedUseScrcpy;
    const initArgs = {
        ...'string' == typeof extracted.deviceId ? {
            deviceId: extracted.deviceId
        } : {},
        ...void 0 !== useScrcpy ? {
            useScrcpy
        } : {},
        ...void 0 !== scrcpyVideoBitRate ? {
            scrcpyVideoBitRate
        } : {},
        ...extractAgentBehaviorInitArgs(extracted) ?? {}
    };
    return Object.keys(initArgs).length > 0 ? initArgs : void 0;
}
class AndroidMidsceneTools extends BaseMidsceneTools {
    getCliReportSessionName() {
        return 'midscene-android';
    }
    createTemporaryDevice() {
        return new AndroidDevice('temp-for-action-space', {});
    }
    async ensureAgent(initArgs) {
        const deviceId = initArgs?.deviceId;
        const nextSignature = getAgentInitArgsSignature(initArgs);
        if (this.agent && shouldRebuildAgentForInitArgs(this.lastInitArgsSignature, nextSignature)) {
            try {
                await this.agent.destroy?.();
            } catch (error) {
                debug('Failed to destroy agent during cleanup:', error);
            }
            this.agent = void 0;
        }
        if (this.agent) return this.agent;
        debug('Creating Android agent with deviceId:', deviceId || 'auto-detect');
        const reportOptions = this.readCliReportAgentOptions();
        const agent = await agentFromAdbDevice(deviceId, {
            autoDismissKeyboard: false,
            ...extractAgentBehaviorInitArgs(initArgs) ?? {},
            ...initArgs?.useScrcpy ? {
                scrcpyConfig: {
                    enabled: true,
                    ...void 0 !== initArgs.scrcpyVideoBitRate ? {
                        videoBitRate: initArgs.scrcpyVideoBitRate
                    } : {}
                }
            } : {},
            ...reportOptions ?? {}
        });
        this.agent = agent;
        this.lastInitArgsSignature = nextSignature;
        return agent;
    }
    preparePlatformTools() {
        return [
            {
                name: 'android_connect',
                description: 'Connect to Android device via ADB. If deviceId not provided, uses the first available device.',
                schema: this.getAgentInitArgSchema(),
                cli: this.getAgentInitArgCliMetadata(),
                handler: async (args)=>{
                    const initArgs = this.extractAgentInitParam(args);
                    const deviceId = initArgs?.deviceId;
                    const reportSession = this.createNewCliReportSession(deviceId ?? 'auto');
                    this.commitCliReportSession(reportSession);
                    if (this.agent) {
                        try {
                            await this.agent.destroy?.();
                        } catch (error) {
                            debug('Failed to destroy agent during connect:', error);
                        }
                        this.agent = void 0;
                        this.lastInitArgsSignature = void 0;
                    }
                    const agent = await this.ensureAgent(initArgs);
                    const screenshot = await agent.page.screenshotBase64();
                    return {
                        content: [
                            {
                                type: 'text',
                                text: `Connected to Android device${deviceId ? `: ${deviceId}` : ' (auto-detected)'}`
                            },
                            ...this.buildScreenshotContent(screenshot)
                        ],
                        isError: false
                    };
                }
            },
            {
                name: 'android_disconnect',
                description: 'Disconnect from current Android device and release ADB resources',
                schema: {},
                handler: this.createDisconnectHandler('Android device')
            }
        ];
    }
    constructor(...args){
        super(...args), agent_tools_define_property(this, "lastInitArgsSignature", void 0), agent_tools_define_property(this, "initArgSpec", {
            namespace: 'android',
            shape: {
                deviceId: z.string().optional().describe('Android device ID (from adb devices)'),
                useScrcpy: z.boolean().optional().describe('Enable scrcpy accelerated screenshots'),
                scrcpyVideoBitRate: z.number().int().positive().optional().describe('scrcpy H.264 video bitrate in bits per second; providing this option also enables scrcpy. Default with --use-scrcpy: 100000000 (100 Mbps). Changing it trades encoded bandwidth against screenshot detail; tune it only from independent transport measurements, not from a freshness-timeout warning alone.'),
                ...agentBehaviorInitArgShape
            },
            cli: {
                preferBareKeys: true
            },
            adapt: adaptAndroidInitArgs
        });
    }
}
const tools = new AndroidMidsceneTools();
runToolsCLI(tools, 'midscene-android', {
    stripPrefix: 'android_',
    version: "1.12.3",
    extraCommands: createReportCliCommands()
}).catch((e)=>{
    process.exit(reportCLIError(e));
});
