"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.AndroidDevice = exports.Android = void 0;

var _debug = _interopRequireDefault(require("debug"));

var _events = require("events");

var _fs = _interopRequireDefault(require("fs"));

var _os = _interopRequireDefault(require("os"));

var _path = _interopRequireDefault(require("path"));

var ws = _interopRequireWildcard(require("ws"));

var _utils = require("../../utils");

var _fileUtils = require("../../utils/fileUtils");

var _browserContext = require("../browserContext");

var _progress = require("../progress");

var _crBrowser = require("../chromium/crBrowser");

var _helper = require("../helper");

var _transport = require("../../protocol/transport");

var _debugLogger = require("../../common/debugLogger");

var _processLauncher = require("../../utils/processLauncher");

var _timeoutSettings = require("../../common/timeoutSettings");

var _instrumentation = require("../instrumentation");

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/**
 * 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.
 */
const ARTIFACTS_FOLDER = _path.default.join(_os.default.tmpdir(), 'playwright-artifacts-');

class Android extends _instrumentation.SdkObject {
  constructor(backend, playwrightOptions) {
    super(playwrightOptions.rootSdkObject, 'android');
    this._backend = void 0;
    this._devices = new Map();
    this._timeoutSettings = void 0;
    this._playwrightOptions = void 0;
    this._backend = backend;
    this._playwrightOptions = playwrightOptions;
    this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
  }

  setDefaultTimeout(timeout) {
    this._timeoutSettings.setDefaultTimeout(timeout);
  }

  async devices(options) {
    const devices = (await this._backend.devices(options)).filter(d => d.status === 'device');
    const newSerials = new Set();

    for (const d of devices) {
      newSerials.add(d.serial);
      if (this._devices.has(d.serial)) continue;
      const device = await AndroidDevice.create(this, d, options);

      this._devices.set(d.serial, device);
    }

    for (const d of this._devices.keys()) {
      if (!newSerials.has(d)) this._devices.delete(d);
    }

    return [...this._devices.values()];
  }

  _deviceClosed(device) {
    this._devices.delete(device.serial);
  }

}

exports.Android = Android;

class AndroidDevice extends _instrumentation.SdkObject {
  constructor(android, backend, model, options) {
    super(android, 'android-device');
    this._backend = void 0;
    this.model = void 0;
    this.serial = void 0;
    this._options = void 0;
    this._driverPromise = void 0;
    this._lastId = 0;
    this._callbacks = new Map();
    this._pollingWebViews = void 0;
    this._timeoutSettings = void 0;
    this._webViews = new Map();
    this._browserConnections = new Set();
    this._android = void 0;
    this._isClosed = false;
    this._android = android;
    this._backend = backend;
    this.model = model;
    this.serial = backend.serial;
    this._options = options;
    this._timeoutSettings = new _timeoutSettings.TimeoutSettings(android._timeoutSettings);
  }

  static async create(android, backend, options) {
    await backend.init();
    const model = await backend.runCommand('shell:getprop ro.product.model');
    const device = new AndroidDevice(android, backend, model.toString().trim(), options);
    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) {
    const result = await this._backend.runCommand(`shell:${command}`);
    await this._refreshWebViews();
    return result;
  }

  async open(command) {
    return await this._backend.open(`${command}`);
  }

  async screenshot() {
    return await this._backend.runCommand(`shell:screencap -p`);
  }

  async _driver() {
    if (!this._driverPromise) this._driverPromise = this._installDriver();
    return this._driverPromise;
  }

  async _installDriver() {
    (0, _debug.default)('pw:android')('Stopping the old driver');
    await this.shell(`am force-stop com.microsoft.playwright.androiddriver`); // uninstall and install driver on every excution

    if (!this._options.omitDriverInstall) {
      (0, _debug.default)('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`);
      (0, _debug.default)('pw:android')('Installing the new driver');

      for (const file of ['android-driver.apk', 'android-driver-target.apk']) await this.installApk(await _fs.default.promises.readFile(require.resolve(`../../../bin/${file}`)));
    } else {
      (0, _debug.default)('pw:android')('Skipping the driver installation');
    }

    (0, _debug.default)('pw:android')('Starting the new driver');
    this.shell('am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner').catch(e => (0, _debug.default)('pw:android')(e));
    const socket = await this._waitForLocalAbstract('playwright_android_driver_socket');
    const transport = new _transport.PipeTransport(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);
    };

    return transport;
  }

  async _waitForLocalAbstract(socketName) {
    let socket;
    (0, _debug.default)('pw:android')(`Polling the socket localabstract:${socketName}`);

    while (!socket) {
      try {
        socket = await this._backend.open(`localabstract:${socketName}`);
      } catch (e) {
        await new Promise(f => setTimeout(f, 250));
      }
    }

    (0, _debug.default)('pw:android')(`Connected to localabstract:${socketName}`);
    return socket;
  }

  async send(method, params = {}) {
    // Patch the timeout in!
    params.timeout = this._timeoutSettings.timeout(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() {
    this._isClosed = true;
    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();

    this._android._deviceClosed(this);

    this.emit(AndroidDevice.Events.Closed);
  }

  async launchBrowser(pkg = 'com.android.chrome', options) {
    (0, _debug.default)('pw:android')('Force-stopping', pkg);
    await this._backend.runCommand(`shell:am force-stop ${pkg}`);
    const socketName = (0, _utils.isUnderTest)() ? 'webview_devtools_remote_playwright_test' : 'playwright-' + (0, _utils.createGuid)();
    const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
    (0, _debug.default)('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`);
    return await this._connectToBrowser(socketName, options);
  }

  async connectToWebView(socketName) {
    const webView = this._webViews.get(socketName);

    if (!webView) throw new Error('WebView has been closed');
    return await this._connectToBrowser(socketName);
  }

  async _connectToBrowser(socketName, options = {}) {
    const socket = await this._waitForLocalAbstract(socketName);
    const androidBrowser = new AndroidBrowser(this, socket);
    await androidBrowser._init();

    this._browserConnections.add(androidBrowser);

    const artifactsDir = await _fs.default.promises.mkdtemp(ARTIFACTS_FOLDER);

    const cleanupArtifactsDir = async () => {
      const errors = await (0, _fileUtils.removeFolders)([artifactsDir]);

      for (let i = 0; i < (errors || []).length; ++i) (0, _debug.default)('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
    };

    _processLauncher.gracefullyCloseSet.add(cleanupArtifactsDir);

    socket.on('close', async () => {
      _processLauncher.gracefullyCloseSet.delete(cleanupArtifactsDir);

      cleanupArtifactsDir().catch(e => (0, _debug.default)('pw:android')(`could not cleanup artifacts dir: ${e}`));
    });
    const browserOptions = { ...this._android._playwrightOptions,
      name: 'clank',
      isChromium: true,
      slowMo: 0,
      persistent: { ...options,
        noDefaultViewport: true
      },
      artifactsDir,
      downloadsPath: artifactsDir,
      tracesDir: artifactsDir,
      browserProcess: new ClankBrowserProcess(androidBrowser),
      proxy: options.proxy,
      protocolLogger: _helper.helper.debugProtocolLogger(),
      browserLogsCollector: new _debugLogger.RecentLogsCollector()
    };
    (0, _browserContext.validateBrowserContextOptions)(options, browserOptions);
    const browser = await _crBrowser.CRBrowser.connect(androidBrowser, browserOptions);
    const controller = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), this);
    const defaultContext = browser._defaultContext;
    await controller.run(async progress => {
      await defaultContext._loadDefaultContextAsIs(progress);
    });
    return defaultContext;
  }

  webViews() {
    return [...this._webViews.values()];
  }

  async installApk(content, options) {
    const args = options && options.args ? options.args : ['-r', '-t', '-S'];
    (0, _debug.default)('pw:android')('Opening install socket');
    const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`);
    (0, _debug.default)('pw:android')('Writing driver bytes: ' + content.length);
    await installSocket.write(content);
    const success = await new Promise(f => installSocket.on('data', f));
    (0, _debug.default)('pw:android')('Written driver bytes: ' + success);
    installSocket.close();
  }

  async push(content, path, mode = 0o644) {
    const socket = await this._backend.open(`sync:`);

    const sendHeader = async (command, length) => {
      const buffer = Buffer.alloc(command.length + 4);
      buffer.write(command, 0);
      buffer.writeUInt32LE(length, command.length);
      await socket.write(buffer);
    };

    const send = async (command, data) => {
      await sendHeader(command, data.length);
      await socket.write(data);
    };

    await send('SEND', Buffer.from(`${path},${mode}`));
    const maxChunk = 65535;

    for (let i = 0; i < content.length; i += maxChunk) await send('DATA', content.slice(i, i + maxChunk));

    await sendHeader('DONE', Date.now() / 1000 | 0);
    const result = await new Promise(f => socket.once('data', f));
    const code = result.slice(0, 4).toString();
    if (code !== 'OKAY') throw new Error('Could not push: ' + code);
    socket.close();
  }

  async _refreshWebViews() {
    // possible socketName, eg: webview_devtools_remote_32327, webview_devtools_remote_32327_zeus, webview_devtools_remote_zeus
    const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
    if (this._isClosed) return;
    const socketNames = new Set();

    for (const line of sockets) {
      const matchSocketName = line.match(/[^@]+@(.*?webview_devtools_remote_?.*)/);
      if (!matchSocketName) continue;
      const socketName = matchSocketName[1];
      socketNames.add(socketName);
      if (this._webViews.has(socketName)) continue; // possible line: 0000000000000000: 00000002 00000000 00010000 0001 01 5841881 @webview_devtools_remote_zeus
      // the result: match[1] = ''

      const match = line.match(/[^@]+@.*?webview_devtools_remote_?(\d*)/);
      let pid = -1;
      if (match && match[1]) pid = +match[1];
      const pkg = await this._extractPkg(pid);
      if (this._isClosed) return;
      const webView = {
        pid,
        pkg,
        socketName
      };

      this._webViews.set(socketName, webView);

      this.emit(AndroidDevice.Events.WebViewAdded, webView);
    }

    for (const p of this._webViews.keys()) {
      if (!socketNames.has(p)) {
        this._webViews.delete(p);

        this.emit(AndroidDevice.Events.WebViewRemoved, p);
      }
    }
  }

  async _extractPkg(pid) {
    let pkg = '';
    if (pid === -1) return pkg;
    const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');

    for (const proc of procs) {
      const match = proc.match(/[^\s]+\s+(\d+).*$/);
      if (!match) continue;
      pkg = proc.substring(proc.lastIndexOf(' ') + 1);
    }

    return pkg;
  }

}

exports.AndroidDevice = AndroidDevice;
AndroidDevice.Events = {
  WebViewAdded: 'webViewAdded',
  WebViewRemoved: 'webViewRemoved',
  Closed: 'closed'
};

class AndroidBrowser extends _events.EventEmitter {
  constructor(device, socket) {
    super();
    this.device = void 0;
    this._socket = void 0;
    this._receiver = void 0;
    this._waitForNextTask = (0, _utils.makeWaitForNextTask)();
    this.onmessage = void 0;
    this.onclose = void 0;
    this.setMaxListeners(0);
    this.device = device;
    this._socket = socket;

    this._socket.on('close', () => {
      this._waitForNextTask(() => {
        if (this.onclose) this.onclose();
      });
    });

    this._receiver = new ws.Receiver();

    this._receiver.on('message', message => {
      this._waitForNextTask(() => {
        if (this.onmessage) this.onmessage(JSON.parse(message));
      });
    });
  }

  async _init() {
    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() {
    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 = void 0;
    this.onclose = void 0;
    this._browser = browser;
  }

  async kill() {}

  async close() {
    await this._browser.close();
  }

}