UNPKG

io.appium.settings

Version:
216 lines 9.53 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.setGeoLocation = setGeoLocation; exports.getGeoLocation = getGeoLocation; exports.refreshGeoLocationCache = refreshGeoLocationCache; const lodash_1 = __importDefault(require("lodash")); const constants_js_1 = require("../constants.js"); const teen_process_1 = require("teen_process"); const bluebird_1 = __importDefault(require("bluebird")); const logger_js_1 = require("../logger.js"); const DEFAULT_SATELLITES_COUNT = 12; const DEFAULT_ALTITUDE = 0.0; const LOCATION_TRACKER_TAG = 'LocationTracker'; const GPS_CACHE_REFRESHED_LOGS = [ 'The current location has been successfully retrieved from Play Services', 'The current location has been successfully retrieved from Location Manager' ]; const GPS_COORDINATES_PATTERN = /data="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)"/; /** * @typedef {Object} Location * @property {number|string} longitude - Valid longitude value. * @property {number|string} latitude - Valid latitude value. * @property {number|string|null} [altitude] - Valid altitude value. * @property {number|string|null} [satellites=12] - Number of satellites being tracked (1-12). * This value is ignored on real devices. * @property {number|string|null} [speed] - Valid speed value. * https://developer.android.com/reference/android/location/Location#setSpeed(float) * @property {number|string|null} [bearing] - Valid bearing value. * https://developer.android.com/reference/android/location/Location#setBearing(float) * @property {number|string|null} [accuracy] - Valid accuracy value. * https://developer.android.com/reference/android/location/Location#setAccuracy(float), * https://developer.android.com/reference/android/location/Criteria * Should be greater than 0.0 meters/second for real devices or 0.0 knots * for emulators. */ /** * Emulate geolocation coordinates on the device under test. * * @this {import('../client').SettingsApp} * @param {Location} location - Location object. The `altitude` value is ignored * while mocking the position. * @param {boolean} [isEmulator=false] - Set it to true if the device under test * is an emulator rather than a real device. */ async function setGeoLocation(location, isEmulator = false) { const formatLocationValue = (valueName, isRequired = true) => { if (lodash_1.default.isNil(location[valueName])) { if (isRequired) { throw new Error(`${valueName} must be provided`); } return null; } const floatValue = parseFloat(location[valueName]); if (!isNaN(floatValue)) { return `${lodash_1.default.ceil(floatValue, 5)}`; } if (isRequired) { throw new Error(`${valueName} is expected to be a valid float number. ` + `'${location[valueName]}' is given instead`); } return null; }; const longitude = /** @type {string} */ (formatLocationValue('longitude')); const latitude = /** @type {string} */ (formatLocationValue('latitude')); const altitude = formatLocationValue('altitude', false); const speed = formatLocationValue('speed', false); const bearing = formatLocationValue('bearing', false); const accuracy = formatLocationValue('accuracy', false); if (isEmulator) { /** @type {string[]} */ const args = [longitude, latitude]; if (!lodash_1.default.isNil(altitude)) { args.push(altitude); } const satellites = parseInt(`${location.satellites}`, 10); if (!Number.isNaN(satellites) && satellites > 0 && satellites <= 12) { if (args.length < 3) { args.push(`${DEFAULT_ALTITUDE}`); } args.push(`${satellites}`); } if (!lodash_1.default.isNil(speed)) { if (args.length < 3) { args.push(`${DEFAULT_ALTITUDE}`); } if (args.length < 4) { args.push(`${DEFAULT_SATELLITES_COUNT}`); } args.push(speed); } await this.adb.resetTelnetAuthToken(); await this.adb.adbExec(['emu', 'geo', 'fix', ...args]); // A workaround for https://code.google.com/p/android/issues/detail?id=206180 await this.adb.adbExec(['emu', 'geo', 'fix', ...(args.map((arg) => arg.replace('.', ',')))]); } else { const args = [ 'am', (await this.adb.getApiLevel() >= 26) ? 'start-foreground-service' : 'startservice', '-e', 'longitude', longitude, '-e', 'latitude', latitude, ]; if (!lodash_1.default.isNil(altitude)) { args.push('-e', 'altitude', altitude); } if (!lodash_1.default.isNil(speed)) { if (lodash_1.default.toNumber(speed) < 0) { throw new Error(`${speed} is expected to be 0.0 or greater.`); } args.push('-e', 'speed', speed); } if (!lodash_1.default.isNil(bearing)) { if (!lodash_1.default.inRange(lodash_1.default.toNumber(bearing), 0, 360)) { throw new Error(`${accuracy} is expected to be in [0, 360) range.`); } args.push('-e', 'bearing', bearing); } if (!lodash_1.default.isNil(accuracy)) { if (lodash_1.default.toNumber(accuracy) < 0) { throw new Error(`${accuracy} is expected to be 0.0 or greater.`); } args.push('-e', 'accuracy', accuracy); } args.push(constants_js_1.LOCATION_SERVICE); await this.adb.shell(args); } } /** * Get the current cached GPS location from the device under test. * * @this {import('../client').SettingsApp} * @returns {Promise<Location>} The current location * @throws {Error} If the current location cannot be retrieved */ async function getGeoLocation() { const output = await this.checkBroadcast([ '-n', constants_js_1.LOCATION_RECEIVER, '-a', constants_js_1.LOCATION_RETRIEVAL_ACTION, ], 'retrieve geolocation', true); const match = GPS_COORDINATES_PATTERN.exec(output); if (!match) { throw new Error(`Cannot parse the actual location values from the command output: ${output}`); } const location = { latitude: match[1], longitude: match[2], altitude: match[3], }; this.log.debug(logger_js_1.LOG_PREFIX, `Got geo coordinates: ${JSON.stringify(location)}`); return location; } /** * Sends an async request to refresh the GPS cache. * This feature only works if the device under test has * Google Play Services installed. In case the vanilla * LocationManager is used the device API level must be at * version 30 (Android R) or higher. * * @this {import('../client').SettingsApp} * @param {number} timeoutMs The maximum number of milliseconds * to block until GPS cache is refreshed. Providing zero or a negative * value to it skips waiting completely. * * @throws {Error} If the GPS cache cannot be refreshed. */ async function refreshGeoLocationCache(timeoutMs = 20000) { await this.requireRunning({ shouldRestoreCurrentApp: true }); let logcatMonitor; let monitoringPromise; if (timeoutMs > 0) { const cmd = [ ...this.adb.executable.defaultArgs, 'logcat', '-s', LOCATION_TRACKER_TAG, ]; logcatMonitor = new teen_process_1.SubProcess(this.adb.executable.path, cmd); const timeoutErrorMsg = `The GPS cache has not been refreshed within ${timeoutMs}ms timeout. ` + `Please make sure the device under test has Appium Settings app installed and running. ` + `Also, it is required that the device has Google Play Services installed or is running ` + `Android 10+ otherwise.`; monitoringPromise = new bluebird_1.default((resolve, reject) => { setTimeout(() => reject(new Error(timeoutErrorMsg)), timeoutMs); logcatMonitor.on('exit', () => reject(new Error(timeoutErrorMsg))); ['lines-stderr', 'lines-stdout'].map((evt) => logcatMonitor.on(evt, (lines) => { if (lines.some((line) => GPS_CACHE_REFRESHED_LOGS.some((x) => line.includes(x)))) { resolve(); } })); }); await logcatMonitor.start(0); } await this.checkBroadcast([ '-n', constants_js_1.LOCATION_RECEIVER, '-a', constants_js_1.LOCATION_RETRIEVAL_ACTION, '--ez', 'forceUpdate', 'true', ], 'refresh GPS cache', false); if (logcatMonitor && monitoringPromise) { const startMs = performance.now(); this.log.debug(logger_js_1.LOG_PREFIX, `Waiting up to ${timeoutMs}ms for the GPS cache to be refreshed`); try { await monitoringPromise; this.log.info(logger_js_1.LOG_PREFIX, `The GPS cache has been successfully refreshed after ` + `${(performance.now() - startMs).toFixed(0)}ms`); } finally { if (logcatMonitor.isRunning) { await logcatMonitor.stop(); } } } else { this.log.info(logger_js_1.LOG_PREFIX, 'The request to refresh the GPS cache has been sent. Skipping waiting for its result.'); } } //# sourceMappingURL=geolocation.js.map