"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;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
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;
    }
    getEmulatorImages(adbDevicesOutput) {
        return __awaiter(this, void 0, void 0, function* () {
            const availableEmulatorsOutput = yield this.getEmulatorImagesCore();
            const avds = availableEmulatorsOutput.devices;
            const runningEmulatorIds = yield this.getRunningEmulatorIds(adbDevicesOutput);
            const runningEmulators = yield (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,
            };
        });
    }
    getRunningEmulatorIds(adbDevicesOutput) {
        return __awaiter(this, void 0, void 0, function* () {
            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;
        });
    }
    getRunningEmulatorName(emulatorId) {
        return __awaiter(this, void 0, void 0, function* () {
            const imageIdentifier = yield 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) {
            // Check https://developer.android.com/studio/releases/sdk-tools.html (25.3.0)
            // Since this version of SDK tools, the emulator is a separate package.
            // However the emulator executable still exists in the "tools" dir.
            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);
                // old output should look like:
                // Android Console: type 'help' for a list of commands
                // OK
                // <Name of image>
                // OK
                // new output should look like:
                // Android Console: type 'help' for a list of commands
                // OK
                // a\u001b[K\u001b[Dav\u001b[K\u001b[D\u001b[Davd\u001b...
                // <Name of image>
                // OK
                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];
        }
    }
    getEmulatorImagesCore() {
        return __awaiter(this, void 0, void 0, function* () {
            let result = null;
            let devices = [];
            let errors = [];
            const canExecuteAvdManagerCommand = yield this.canExecuteAvdManagerCommand();
            if (!canExecuteAvdManagerCommand) {
                errors = [
                    "Unable to execute avdmanager, ensure JAVA_HOME is set and points to correct directory",
                ];
            }
            if (canExecuteAvdManagerCommand) {
                result = yield this.$childProcess.trySpawnFromCloseEvent(this.pathToAvdManagerExecutable, ["list", "avds"]);
            }
            else if (this.pathToAndroidExecutable &&
                this.$fs.exists(this.pathToAndroidExecutable)) {
                result = yield 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 };
        });
    }
    canExecuteAvdManagerCommand() {
        return __awaiter(this, void 0, void 0, function* () {
            let canExecute = false;
            if (this.pathToAvdManagerExecutable &&
                this.$fs.exists(this.pathToAvdManagerExecutable)) {
                if (process.env.JAVA_HOME) {
                    // In case JAVA_HOME is set, but it points to incorrect directory (i.e. there's no java in $JAVA_HOME/bin/java), avdmanager will fail
                    // no matter if you have correct java in PATH.
                    canExecute = !!(yield this.$sysInfo.getJavaVersionFromJavaHome());
                }
                else {
                    canExecute = !!(yield this.$sysInfo.getJavaVersionFromPath());
                }
            }
            return canExecute;
        });
    }
    getRunningEmulatorData(runningEmulatorId, availableEmulators) {
        return __awaiter(this, void 0, void 0, function* () {
            const imageIdentifier = yield 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) {
            // In some cases `avdmanager list avds` command prints:
            //	`The following Android Virtual Devices could not be loaded:
            //	Name: Pixel_2_XL_API_28
            //	Path: /Users/<username>/.android/avd/Pixel_2_XL_API_28.avd
            //	Error: Google pixel_2_xl no longer exists as a device`
            // These devices sometimes are valid so try to parse them.
            // Also these devices are printed at the end of the output and are separated with 2 new lines from the valid devices output.
            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);
        // Split by `\n`, not EOL as the avdmanager and android executables print results with `\n` only even on Windows
        _.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) {
        // The lines should be trimmed after the split because the output has \r\n and when using split(EOL) on mac each line ends with \r.
        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);
        }
    }
}
__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);
exports.AndroidVirtualDeviceService = AndroidVirtualDeviceService;
yok_1.injector.register("androidVirtualDeviceService", AndroidVirtualDeviceService);
//# sourceMappingURL=android-virtual-device-service.js.map