"use strict";
/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidDevice = exports.Android = void 0;
const debug = require("debug");
const events_1 = require("events");
const fs = require("fs");
const util = require("util");
const ws = require("ws");
const utils_1 = require("../../utils/utils");
const browserContext_1 = require("../browserContext");
const progress_1 = require("../progress");
const crBrowser_1 = require("../chromium/crBrowser");
const helper_1 = require("../helper");
const transport_1 = require("../../protocol/transport");
const debugLogger_1 = require("../../utils/debugLogger");
const timeoutSettings_1 = require("../../utils/timeoutSettings");
const readFileAsync = util.promisify(fs.readFile);
class Android {
    constructor(backend) {
        this._backend = backend;
        this._timeoutSettings = new timeoutSettings_1.TimeoutSettings();
    }
    setDefaultTimeout(timeout) {
        this._timeoutSettings.setDefaultTimeout(timeout);
    }
    async devices() {
        const devices = await this._backend.devices();
        return await Promise.all(devices.map(d => AndroidDevice.create(this, d)));
    }
}
exports.Android = Android;
class AndroidDevice extends events_1.EventEmitter {
    constructor(android, backend, model) {
        super();
        this._lastId = 0;
        this._callbacks = new Map();
        this._webViews = new Map();
        this._browserConnections = new Set();
        this._backend = backend;
        this.model = model;
        this.serial = backend.serial;
        this._timeoutSettings = new timeoutSettings_1.TimeoutSettings(android._timeoutSettings);
    }
    static async create(android, backend) {
        await backend.init();
        const model = await backend.runCommand('shell:getprop ro.product.model');
        const device = new AndroidDevice(android, backend, model.trim());
        await device._init();
        return device;
    }
    async _init() {
        await this._refreshWebViews();
        const poll = () => {
            this._pollingWebViews = setTimeout(() => this._refreshWebViews().then(poll).catch(() => { }), 500);
        };
        poll();
    }
    setDefaultTimeout(timeout) {
        this._timeoutSettings.setDefaultTimeout(timeout);
    }
    async shell(command) {
        return await this._backend.runCommand(`shell:${command}`);
    }
    async _driver() {
        if (this._driverPromise)
            return this._driverPromise;
        let callback;
        this._driverPromise = new Promise(f => callback = f);
        debug('pw:android')('Stopping the old driver');
        await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
        debug('pw:android')('Uninstalling the old driver');
        await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`);
        await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`);
        debug('pw:android')('Installing the new driver');
        for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
            debug('pw:android')('Reading ' + require.resolve(`../../../bin/${file}`));
            const driverFile = await readFileAsync(require.resolve(`../../../bin/${file}`));
            debug('pw:android')('Opening install socket');
            const installSocket = await this._backend.open(`shell:cmd package install -r -t -S ${driverFile.length}`);
            debug('pw:android')('Writing driver bytes: ' + driverFile.length);
            await installSocket.write(driverFile);
            const success = await new Promise(f => installSocket.on('data', f));
            debug('pw:android')('Written driver bytes: ' + success);
        }
        debug('pw:android')('Starting the new driver');
        this.shell(`am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner`);
        debug('pw:android')('Polling the socket');
        let socket;
        while (!socket) {
            try {
                socket = await this._backend.open(`localabstract:playwright_android_driver_socket`);
            }
            catch (e) {
                await new Promise(f => setTimeout(f, 100));
            }
        }
        debug('pw:android')('Connected to driver');
        const transport = new transport_1.Transport(socket, socket, socket, 'be');
        transport.onmessage = message => {
            const response = JSON.parse(message);
            const { id, result, error } = response;
            const callback = this._callbacks.get(id);
            if (!callback)
                return;
            if (error)
                callback.reject(new Error(error));
            else
                callback.fulfill(result);
            this._callbacks.delete(id);
        };
        callback(transport);
        return this._driverPromise;
    }
    async send(method, params) {
        const driver = await this._driver();
        const id = ++this._lastId;
        const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
        driver.send(JSON.stringify({ id, method, params }));
        return result;
    }
    async close() {
        if (this._pollingWebViews)
            clearTimeout(this._pollingWebViews);
        for (const connection of this._browserConnections)
            await connection.close();
        if (this._driverPromise) {
            const driver = await this._driver();
            driver.close();
        }
        await this._backend.close();
    }
    async launchBrowser(pkg = 'com.android.chrome', options = {}) {
        debug('pw:android')('Force-stopping', pkg);
        await this._backend.runCommand(`shell:am force-stop ${pkg}`);
        const socketName = utils_1.createGuid();
        const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
        debug('pw:android')('Starting', pkg, commandLine);
        await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
        await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
        debug('pw:android')('Polling for socket', socketName);
        while (true) {
            const net = await this._backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
            if (net)
                break;
            await new Promise(f => setTimeout(f, 100));
        }
        debug('pw:android')('Got the socket, connecting');
        return await this._connectToBrowser(socketName, options);
    }
    connectToWebView(pid) {
        const webView = this._webViews.get(pid);
        if (!webView)
            throw new Error('WebView has been closed');
        return this._connectToBrowser(`webview_devtools_remote_${pid}`);
    }
    async _connectToBrowser(socketName, options = {}) {
        const androidBrowser = new AndroidBrowser(this, socketName);
        await androidBrowser._open();
        this._browserConnections.add(androidBrowser);
        const browserOptions = {
            name: 'clank',
            slowMo: 0,
            persistent: { ...options, noDefaultViewport: true },
            downloadsPath: undefined,
            browserProcess: new ClankBrowserProcess(androidBrowser),
            proxy: options.proxy,
            protocolLogger: helper_1.helper.debugProtocolLogger(),
            browserLogsCollector: new debugLogger_1.RecentLogsCollector()
        };
        browserContext_1.validateBrowserContextOptions(options, browserOptions);
        const browser = await crBrowser_1.CRBrowser.connect(androidBrowser, browserOptions);
        const controller = new progress_1.ProgressController();
        await controller.run(async (progress) => {
            await browser._defaultContext._loadDefaultContext(progress);
        });
        return browser._defaultContext;
    }
    webViews() {
        return [...this._webViews.values()];
    }
    async _refreshWebViews() {
        const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).split('\n');
        const newPids = new Set();
        for (const line of sockets) {
            const match = line.match(/[^@]+@webview_devtools_remote_(\d+)/);
            if (!match)
                continue;
            const pid = +match[1];
            newPids.add(pid);
        }
        for (const pid of newPids) {
            if (this._webViews.has(pid))
                continue;
            const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).split('\n');
            let pkg = '';
            for (const proc of procs) {
                const match = proc.match(/[^\s]+\s+(\d+).*$/);
                if (!match)
                    continue;
                const p = match[1];
                if (+p !== pid)
                    continue;
                pkg = proc.substring(proc.lastIndexOf(' '));
            }
            const webView = { pid, pkg };
            this._webViews.set(pid, webView);
            this.emit(AndroidDevice.Events.WebViewAdded, webView);
        }
        for (const p of this._webViews.keys()) {
            if (!newPids.has(p)) {
                this._webViews.delete(p);
                this.emit(AndroidDevice.Events.WebViewRemoved, p);
            }
        }
    }
}
exports.AndroidDevice = AndroidDevice;
AndroidDevice.Events = {
    WebViewAdded: 'webViewAdded',
    WebViewRemoved: 'webViewRemoved',
};
class AndroidBrowser extends events_1.EventEmitter {
    constructor(device, socketName) {
        super();
        this._waitForNextTask = utils_1.makeWaitForNextTask();
        this.device = device;
        this.socketName = socketName;
        this._receiver = new ws.Receiver();
        this._receiver.on('message', message => {
            this._waitForNextTask(() => {
                if (this.onmessage)
                    this.onmessage(JSON.parse(message));
            });
        });
    }
    async _open() {
        this._socket = await this.device._backend.open(`localabstract:${this.socketName}`);
        this._socket.on('close', () => {
            this._waitForNextTask(() => {
                if (this.onclose)
                    this.onclose();
            });
        });
        await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
Upgrade: WebSocket\r
Connection: Upgrade\r
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
Sec-WebSocket-Version: 13\r
\r
`));
        // HTTP Upgrade response.
        await new Promise(f => this._socket.once('data', f));
        // Start sending web frame to receiver.
        this._socket.on('data', data => this._receiver._write(data, 'binary', () => { }));
    }
    async send(s) {
        await this._socket.write(encodeWebFrame(JSON.stringify(s)));
    }
    async close() {
        await this._socket.close();
    }
}
function encodeWebFrame(data) {
    return ws.Sender.frame(Buffer.from(data), {
        opcode: 1,
        mask: true,
        fin: true,
        readOnly: true
    })[0];
}
class ClankBrowserProcess {
    constructor(browser) {
        this._browser = browser;
    }
    async kill() {
    }
    async close() {
        await this._browser.close();
    }
}
//# sourceMappingURL=android.js.map