io.appium.settings
Version:
App for dealing with Android settings
216 lines • 9.53 kB
JavaScript
;
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