"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidVirtualDeviceService = void 0;
const net = require("net");
const path = require("path");
const os_1 = require("os");
const _ = require("lodash");
const os_2 = require("os");
const constants_1 = require("../../constants");
const decorators_1 = require("../../decorators");
const helpers_1 = require("../../helpers");
const constants_2 = require("../../../constants");
const yok_1 = require("../../yok");
class AndroidVirtualDeviceService {
    constructor($androidIniFileParser, $childProcess, $devicePlatformsConstants, $emulatorHelper, $fs, $hostInfo, $sysInfo, $logger) {
        this.$androidIniFileParser = $androidIniFileParser;
        this.$childProcess = $childProcess;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$emulatorHelper = $emulatorHelper;
        this.$fs = $fs;
        this.$hostInfo = $hostInfo;
        this.$sysInfo = $sysInfo;
        this.$logger = $logger;
        this.mapEmulatorIdToImageIdentifier = {};
        this.androidHome = process.env.ANDROID_HOME;
    }
    async getEmulatorImages(adbDevicesOutput) {
        const availableEmulatorsOutput = await this.getEmulatorImagesCore();
        const avds = availableEmulatorsOutput.devices;
        const runningEmulatorIds = await this.getRunningEmulatorIds(adbDevicesOutput);
        const runningEmulators = await (0, helpers_1.settlePromises)(_.map(runningEmulatorIds, (emulatorId) => this.getRunningEmulatorData(emulatorId, avds)));
        const devices = availableEmulatorsOutput.devices.map((emulator) => this.$emulatorHelper.getEmulatorByImageIdentifier(emulator.imageIdentifier, runningEmulators) || emulator);
        return {
            devices,
            errors: availableEmulatorsOutput.errors,
        };
    }
    async getRunningEmulatorIds(adbDevicesOutput) {
        const emulatorIds = _.reduce(adbDevicesOutput, (result, device) => {
            const rx = device.match(constants_1.AndroidVirtualDevice.RUNNING_AVD_EMULATOR_REGEX);
            if (rx && rx[1]) {
                result.push(rx[1]);
            }
            return result;
        }, []);
        return emulatorIds;
    }
    async getRunningEmulatorName(emulatorId) {
        const imageIdentifier = await this.getRunningEmulatorImageIdentifier(emulatorId);
        const iniFilePath = path.join(this.pathToAvdHomeDir, `${imageIdentifier}.ini`);
        const iniFileInfo = this.$androidIniFileParser.parseIniFile(iniFilePath);
        let result = imageIdentifier;
        if (iniFileInfo && iniFileInfo.path) {
            const configIniFileInfo = this.$androidIniFileParser.parseIniFile(path.join(iniFileInfo.path, constants_1.AndroidVirtualDevice.CONFIG_INI_FILE_NAME));
            result =
                (configIniFileInfo && configIniFileInfo.displayName) || imageIdentifier;
        }
        return result;
    }
    startEmulatorArgs(imageIdentifier) {
        return ["-avd", imageIdentifier];
    }
    get pathToEmulatorExecutable() {
        const emulatorExecutableName = "emulator";
        if (this.androidHome) {
            const pathToEmulatorFromAndroidStudio = path.join(this.androidHome, emulatorExecutableName, emulatorExecutableName);
            const realFilePath = this.$hostInfo.isWindows
                ? `${pathToEmulatorFromAndroidStudio}.exe`
                : pathToEmulatorFromAndroidStudio;
            if (this.$fs.exists(realFilePath)) {
                return pathToEmulatorFromAndroidStudio;
            }
            return path.join(this.androidHome, "tools", emulatorExecutableName);
        }
        return emulatorExecutableName;
    }
    getRunningEmulatorImageIdentifier(emulatorId) {
        if (this.mapEmulatorIdToImageIdentifier[emulatorId]) {
            return Promise.resolve(this.mapEmulatorIdToImageIdentifier[emulatorId]);
        }
        const match = emulatorId.match(/^emulator-(\d+)/);
        const portNumber = match && match[1];
        if (!portNumber) {
            return Promise.resolve(null);
        }
        return new Promise((resolveBase) => {
            let isResolved = false;
            let output = "";
            const resolve = (result) => {
                if (!isResolved) {
                    isResolved = true;
                    resolveBase(result);
                }
            };
            const client = net.connect(portNumber, () => {
                client.write(`avd name${os_1.EOL}`);
            });
            const timer = setTimeout(() => {
                this.clearNetConnection(client, timer);
                resolve(null);
            }, 5000);
            client.on("data", (data) => {
                output += data.toString();
                const imageIdentifier = this.getImageIdentifierFromClientOutput(output);
                if (imageIdentifier && !isResolved) {
                    this.mapEmulatorIdToImageIdentifier[emulatorId] = imageIdentifier;
                    this.clearNetConnection(client, timer);
                    resolve(imageIdentifier);
                }
            });
            client.on("error", (error) => {
                this.$logger.trace(`Error while checking emulator identifier for ${emulatorId}. More info: ${error}.`);
                resolve(null);
            });
        });
    }
    detach(deviceInfo) {
        if (this.mapEmulatorIdToImageIdentifier[deviceInfo.identifier]) {
            delete this.mapEmulatorIdToImageIdentifier[deviceInfo.identifier];
        }
    }
    async getEmulatorImagesCore() {
        let result = null;
        let devices = [];
        let errors = [];
        const canExecuteAvdManagerCommand = await this.canExecuteAvdManagerCommand();
        if (!canExecuteAvdManagerCommand) {
            errors = [
                "Unable to execute avdmanager, ensure JAVA_HOME is set and points to correct directory",
            ];
        }
        if (canExecuteAvdManagerCommand) {
            const sanitizedPathToAvdManagerExecutable = this.$hostInfo.isWindows
                ? (0, helpers_1.quoteString)(this.pathToAvdManagerExecutable)
                : this.pathToAvdManagerExecutable;
            result = await this.$childProcess.trySpawnFromCloseEvent(sanitizedPathToAvdManagerExecutable, ["list", "avds"], { shell: this.$hostInfo.isWindows });
        }
        else if (this.pathToAndroidExecutable &&
            this.$fs.exists(this.pathToAndroidExecutable)) {
            result = await this.$childProcess.trySpawnFromCloseEvent(this.pathToAndroidExecutable, ["list", "avd"]);
        }
        if (result && result.stdout) {
            devices = this.parseListAvdsOutput(result.stdout);
            errors = result && result.stderr ? [result.stderr] : [];
        }
        else {
            devices = this.listAvdsFromDirectory();
        }
        return { devices, errors };
    }
    async canExecuteAvdManagerCommand() {
        let canExecute = false;
        if (this.pathToAvdManagerExecutable &&
            this.$fs.exists(this.pathToAvdManagerExecutable)) {
            if (process.env.JAVA_HOME) {
                canExecute = !!(await this.$sysInfo.getJavaVersionFromJavaHome());
            }
            else {
                canExecute = !!(await this.$sysInfo.getJavaVersionFromPath());
            }
        }
        return canExecute;
    }
    async getRunningEmulatorData(runningEmulatorId, availableEmulators) {
        const imageIdentifier = await this.getRunningEmulatorImageIdentifier(runningEmulatorId);
        const runningEmulator = this.$emulatorHelper.getEmulatorByImageIdentifier(imageIdentifier, availableEmulators);
        if (!runningEmulator) {
            return null;
        }
        this.$emulatorHelper.setRunningAndroidEmulatorProperties(runningEmulatorId, runningEmulator);
        return runningEmulator;
    }
    get pathToAvdManagerExecutable() {
        let avdManagerPath = null;
        if (this.androidHome) {
            avdManagerPath = path.join(this.androidHome, "tools", "bin", this.getExecutableName("avdmanager"));
        }
        return avdManagerPath;
    }
    get pathToAndroidExecutable() {
        let androidPath = null;
        if (this.androidHome) {
            androidPath = path.join(this.androidHome, "tools", this.getExecutableName("android"));
        }
        return androidPath;
    }
    get pathToAvdHomeDir() {
        const searchPaths = [
            process.env.ANDROID_AVD_HOME,
            path.join((0, os_2.homedir)(), constants_1.AndroidVirtualDevice.ANDROID_DIR_NAME, constants_1.AndroidVirtualDevice.AVD_DIR_NAME),
        ];
        return searchPaths.find((p) => p && this.$fs.exists(p));
    }
    getConfigurationError() {
        const pathToEmulatorExecutable = this.$hostInfo.isWindows
            ? `${this.pathToEmulatorExecutable}.exe`
            : this.pathToEmulatorExecutable;
        if (!this.$fs.exists(pathToEmulatorExecutable)) {
            return "Unable to find the path to emulator executable and will not be able to start the emulator. Searched paths: [$ANDROID_HOME/tools/emulator, $ANDROID_HOME/emulator/emulator]";
        }
        return null;
    }
    getExecutableName(executable) {
        if (this.$hostInfo.isWindows) {
            return `${executable}.bat`;
        }
        return executable;
    }
    listAvdsFromDirectory() {
        let devices = [];
        if (this.pathToAvdHomeDir && this.$fs.exists(this.pathToAvdHomeDir)) {
            const entries = this.$fs.readDirectory(this.pathToAvdHomeDir);
            devices = _.filter(entries, (e) => e.match(constants_1.AndroidVirtualDevice.AVD_FILES_MASK) !== null)
                .map((e) => e.match(constants_1.AndroidVirtualDevice.AVD_FILES_MASK)[1])
                .map((avdName) => path.join(this.pathToAvdHomeDir, `${avdName}.avd`))
                .map((avdPath) => this.getInfoFromAvd(avdPath))
                .filter((avdInfo) => !!avdInfo)
                .map((avdInfo) => this.convertAvdToDeviceInfo(avdInfo));
        }
        return devices;
    }
    parseListAvdsOutput(output) {
        let devices = [];
        const avdOutput = output.split(constants_1.AndroidVirtualDevice.AVAILABLE_AVDS_MESSAGE);
        const availableDevices = avdOutput && avdOutput[1] && avdOutput[1].trim();
        if (availableDevices) {
            const parts = availableDevices.split(/(?:\r?\n){2}/);
            const items = [parts[0], parts[1]].filter((item) => !!item);
            for (const item of items) {
                const result = item
                    .split(constants_1.AndroidVirtualDevice.AVD_LIST_DELIMITER)
                    .map((singleDeviceOutput) => this.getAvdManagerDeviceInfo(singleDeviceOutput.trim()))
                    .map((avdManagerDeviceInfo) => this.getInfoFromAvd(avdManagerDeviceInfo.path))
                    .filter((avdInfo) => !!avdInfo)
                    .map((avdInfo) => this.convertAvdToDeviceInfo(avdInfo));
                devices = devices.concat(result);
            }
        }
        return devices;
    }
    getAvdManagerDeviceInfo(output) {
        const avdManagerDeviceInfo = Object.create(null);
        _.reduce(output.split("\n"), (result, row) => {
            const [key, value] = row.split(": ").map((part) => part.trim());
            switch (key) {
                case "Name":
                case "Device":
                case "Path":
                case "Target":
                case "Skin":
                case "Sdcard":
                    result[key.toLowerCase()] = value;
                    break;
            }
            return result;
        }, avdManagerDeviceInfo || {});
        return avdManagerDeviceInfo;
    }
    getInfoFromAvd(avdFilePath) {
        const configIniFilePath = path.join(avdFilePath, constants_1.AndroidVirtualDevice.CONFIG_INI_FILE_NAME);
        const configIniFileInfo = this.$androidIniFileParser.parseIniFile(configIniFilePath);
        const iniFilePath = this.getIniFilePath(configIniFileInfo, avdFilePath);
        const iniFileInfo = this.$androidIniFileParser.parseIniFile(iniFilePath);
        _.extend(configIniFileInfo, iniFileInfo);
        if (configIniFileInfo && !configIniFileInfo.avdId) {
            configIniFileInfo.avdId = path
                .basename(avdFilePath)
                .replace(constants_1.AndroidVirtualDevice.AVD_FILE_EXTENSION, "");
        }
        return configIniFileInfo;
    }
    convertAvdToDeviceInfo(avdInfo) {
        return {
            identifier: null,
            imageIdentifier: avdInfo.avdId || avdInfo.displayName,
            displayName: avdInfo.displayName || avdInfo.avdId || avdInfo.device,
            model: avdInfo.device,
            version: this.$emulatorHelper.mapAndroidApiLevelToVersion[avdInfo.target],
            vendor: constants_1.AndroidVirtualDevice.AVD_VENDOR_NAME,
            status: constants_1.NOT_RUNNING_EMULATOR_STATUS,
            errorHelp: this.getConfigurationError(),
            isTablet: false,
            type: constants_1.DeviceTypes.Emulator,
            connectionTypes: [constants_2.DeviceConnectionType.Local],
            platform: this.$devicePlatformsConstants.Android,
        };
    }
    getImageIdentifierFromClientOutput(output) {
        const lines = _.map(output.split(os_1.EOL), (line) => line.trim());
        const firstIndexOfOk = _.indexOf(lines, "OK");
        if (firstIndexOfOk < 0) {
            return null;
        }
        const secondIndexOfOk = _.indexOf(lines, "OK", firstIndexOfOk + 1);
        if (secondIndexOfOk < 0) {
            return null;
        }
        return lines[secondIndexOfOk - 1].trim();
    }
    getIniFilePath(configIniFileInfo, avdFilePath) {
        let result = avdFilePath.replace(constants_1.AndroidVirtualDevice.AVD_FILE_EXTENSION, constants_1.AndroidVirtualDevice.INI_FILE_EXTENSION);
        if (configIniFileInfo && configIniFileInfo.avdId) {
            result = path.join(path.dirname(avdFilePath), `${configIniFileInfo.avdId}${constants_1.AndroidVirtualDevice.INI_FILE_EXTENSION}`);
        }
        return result;
    }
    clearNetConnection(client, timer) {
        if (client) {
            client.removeAllListeners();
            client.destroy();
        }
        if (timer) {
            clearTimeout(timer);
        }
    }
}
exports.AndroidVirtualDeviceService = AndroidVirtualDeviceService;
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "pathToEmulatorExecutable", null);
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "canExecuteAvdManagerCommand", null);
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "pathToAvdManagerExecutable", null);
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "pathToAndroidExecutable", null);
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "pathToAvdHomeDir", null);
__decorate([
    (0, decorators_1.cache)()
], AndroidVirtualDeviceService.prototype, "getConfigurationError", null);
yok_1.injector.register("androidVirtualDeviceService", AndroidVirtualDeviceService);
