UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

229 lines 8.72 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.mobileSetGeolocation = mobileSetGeolocation; exports.mobileRefreshGpsCache = mobileRefreshGpsCache; exports.getGeoLocation = getGeoLocation; exports.mobileGetGeolocation = mobileGetGeolocation; exports.isLocationServicesEnabled = isLocationServicesEnabled; exports.toggleLocationServices = toggleLocationServices; exports.mobileResetGeolocation = mobileResetGeolocation; exports.setMockLocationApp = setMockLocationApp; const lodash_1 = __importDefault(require("lodash")); const support_1 = require("@appium/support"); const node_path_1 = __importDefault(require("node:path")); const bluebird_1 = __importDefault(require("bluebird")); const io_appium_settings_1 = require("io.appium.settings"); const app_management_1 = require("./app-management"); // The value close to zero, but not zero, is needed // to trick JSON generation and send a float value instead of an integer, // This allows strictly-typed clients, like Java, to properly // parse it. Otherwise float 0.0 is always represented as integer 0 in JS. // The value must not be greater than DBL_EPSILON (https://opensource.apple.com/source/Libc/Libc-498/include/float.h) const GEO_EPSILON = Number.MIN_VALUE; const MOCK_APP_IDS_STORE = '/data/local/tmp/mock_apps.json'; /** * @this {import('../driver').AndroidDriver} * @param {import('@appium/types').Location} location * @returns {Promise<import('@appium/types').Location>} */ async function setGeoLocation(location) { await this.settingsApp.setGeoLocation(location, this.isEmulator()); try { return await this.getGeoLocation(); } catch (e) { this.log.warn(`Could not get the current geolocation info: ${ /** @type {Error} */(e).message}`); this.log.warn(`Returning the default zero'ed values`); return { latitude: GEO_EPSILON, longitude: GEO_EPSILON, altitude: GEO_EPSILON, }; } } /** * Set the device geolocation. * * @this {import('../driver').AndroidDriver} * @param {number} latitude Valid latitude value. * @param {number} longitude Valid longitude value. * @param {number} [altitude] Valid altitude value. * @param {number} [satellites] Number of satellites being tracked (1-12). Available for emulators. * @param {number} [speed] Valid speed value. * https://developer.android.com/reference/android/location/Location#setSpeed(float) * @param {number} [bearing] Valid bearing value. Available for real devices. * https://developer.android.com/reference/android/location/Location#setBearing(float) * @param {number} [accuracy] Valid accuracy value. Available for real devices. * https://developer.android.com/reference/android/location/Location#setAccuracy(float), * https://developer.android.com/reference/android/location/Criteria */ async function mobileSetGeolocation(latitude, longitude, altitude, satellites, speed, bearing, accuracy) { await this.settingsApp.setGeoLocation({ latitude, longitude, altitude, satellites, speed, bearing, accuracy }, this.isEmulator()); } /** * 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('../driver').AndroidDriver} * @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. * 20000ms by default. * @returns {Promise<void>} */ async function mobileRefreshGpsCache(timeoutMs) { await this.settingsApp.refreshGeoLocationCache(timeoutMs); } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<import('@appium/types').Location>} */ async function getGeoLocation() { const { latitude, longitude, altitude } = await this.settingsApp.getGeoLocation(); return { latitude: parseFloat(String(latitude)) || GEO_EPSILON, longitude: parseFloat(String(longitude)) || GEO_EPSILON, altitude: parseFloat(String(altitude)) || GEO_EPSILON, }; } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<import('@appium/types').Location>} */ async function mobileGetGeolocation() { return await this.getGeoLocation(); } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<boolean>} */ async function isLocationServicesEnabled() { return (await this.adb.getLocationProviders()).includes('gps'); } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<void>} */ async function toggleLocationServices() { this.log.info('Toggling location services'); const isGpsEnabled = await this.isLocationServicesEnabled(); this.log.debug(`Current GPS state: ${isGpsEnabled}. ` + `The service is going to be ${isGpsEnabled ? 'disabled' : 'enabled'}`); await this.adb.toggleGPSLocationProvider(!isGpsEnabled); } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<void>} */ async function mobileResetGeolocation() { if (this.isEmulator()) { throw new Error('Geolocation reset does not work on emulators'); } await resetMockLocation.bind(this); } // #region Internal helpers /** * @this {import('../driver').AndroidDriver} * @param {string} appId * @returns {Promise<void>} */ async function setMockLocationApp(appId) { try { if ((await this.adb.getApiLevel()) < 23) { await this.adb.shell(['settings', 'put', 'secure', 'mock_location', '1']); } else { await this.adb.shell(['appops', 'set', appId, 'android:mock_location', 'allow']); } } catch (err) { this.log.warn(`Unable to set mock location for app '${appId}': ${err.message}`); return; } try { /** @type {string[]} */ let pkgIds = []; if (await this.adb.fileExists(MOCK_APP_IDS_STORE)) { try { pkgIds = JSON.parse(await this.adb.shell(['cat', MOCK_APP_IDS_STORE])); } catch { } } if (pkgIds.includes(appId)) { return; } pkgIds.push(appId); const tmpRoot = await support_1.tempDir.openDir(); const srcPath = node_path_1.default.posix.join(tmpRoot, node_path_1.default.posix.basename(MOCK_APP_IDS_STORE)); try { await support_1.fs.writeFile(srcPath, JSON.stringify(pkgIds), 'utf8'); await this.adb.push(srcPath, MOCK_APP_IDS_STORE); } finally { await support_1.fs.rimraf(tmpRoot); } } catch (e) { this.log.warn(`Unable to persist mock location app id '${appId}': ${e.message}`); } } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<void>} */ async function resetMockLocation() { try { if ((await this.adb.getApiLevel()) < 23) { await this.adb.shell(['settings', 'put', 'secure', 'mock_location', '0']); return; } const thirdPartyPkgIdsPromise = app_management_1.getThirdPartyPackages.bind(this)(); let pkgIds = []; if (await this.adb.fileExists(MOCK_APP_IDS_STORE)) { try { pkgIds = JSON.parse(await this.adb.shell(['cat', MOCK_APP_IDS_STORE])); } catch { } } const thirdPartyPkgIds = await thirdPartyPkgIdsPromise; // Only include currently installed packages const resultPkgs = lodash_1.default.intersection(pkgIds, thirdPartyPkgIds); if (lodash_1.default.size(resultPkgs) <= 1) { await this.adb.shell([ 'appops', 'set', resultPkgs[0] ?? io_appium_settings_1.SETTINGS_HELPER_ID, 'android:mock_location', 'deny', ]); return; } this.log.debug(`Resetting mock_location permission for the following apps: ${resultPkgs}`); await bluebird_1.default.all(resultPkgs.map((pkgId) => (async () => { try { await this.adb.shell(['appops', 'set', pkgId, 'android:mock_location', 'deny']); } catch { } })())); } catch (err) { this.log.warn(`Unable to reset mock location: ${err.message}`); } } // #endregion Internal helpers //# sourceMappingURL=geolocation.js.map