UNPKG

appium-adb

Version:

Android Debug Bridge interface

686 lines 26.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDeviceProperty = getDeviceProperty; exports.setDeviceProperty = setDeviceProperty; exports.getDeviceSysLanguage = getDeviceSysLanguage; exports.getDeviceSysCountry = getDeviceSysCountry; exports.getDeviceSysLocale = getDeviceSysLocale; exports.getDeviceProductLanguage = getDeviceProductLanguage; exports.getDeviceProductCountry = getDeviceProductCountry; exports.getDeviceProductLocale = getDeviceProductLocale; exports.getModel = getModel; exports.getManufacturer = getManufacturer; exports.getScreenSize = getScreenSize; exports.getScreenDensity = getScreenDensity; exports.setHttpProxy = setHttpProxy; exports.deleteHttpProxy = deleteHttpProxy; exports.setSetting = setSetting; exports.getSetting = getSetting; exports.getTimeZone = getTimeZone; exports.getPlatformVersion = getPlatformVersion; exports.getLocationProviders = getLocationProviders; exports.toggleGPSLocationProvider = toggleGPSLocationProvider; exports.setHiddenApiPolicy = setHiddenApiPolicy; exports.setDefaultHiddenApiPolicy = setDefaultHiddenApiPolicy; exports.getDeviceLanguage = getDeviceLanguage; exports.getDeviceCountry = getDeviceCountry; exports.getDeviceLocale = getDeviceLocale; exports.ensureCurrentLocale = ensureCurrentLocale; exports.setWifiState = setWifiState; exports.setDataState = setDataState; exports.getDeviceIdleWhitelist = getDeviceIdleWhitelist; exports.addToDeviceIdleWhitelist = addToDeviceIdleWhitelist; exports.isAirplaneModeOn = isAirplaneModeOn; exports.setAirplaneMode = setAirplaneMode; exports.setBluetoothOn = setBluetoothOn; exports.setNfcOn = setNfcOn; exports.broadcastAirplaneMode = broadcastAirplaneMode; exports.isWifiOn = isWifiOn; exports.isDataOn = isDataOn; exports.isAnimationOn = isAnimationOn; exports.setAnimationScale = setAnimationScale; exports.getScreenOrientation = getScreenOrientation; const logger_1 = require("../logger"); const lodash_1 = __importDefault(require("lodash")); const asyncbox_1 = require("asyncbox"); const support_1 = require("@appium/support"); const bluebird_1 = __importDefault(require("bluebird")); const ANIMATION_SCALE_KEYS = [ 'animator_duration_scale', 'transition_animation_scale', 'window_animation_scale', ]; const HIDDEN_API_POLICY_KEYS = [ 'hidden_api_policy_pre_p_apps', 'hidden_api_policy_p_apps', 'hidden_api_policy', ]; /** * Get the particular property of the device under test. * * @param property - The name of the property. This name should * be known to _adb shell getprop_ tool. * * @returns The value of the given property. */ async function getDeviceProperty(property) { const stdout = await this.shell(['getprop', property]); const val = stdout.trim(); logger_1.log.debug(`Current device property '${property}': ${val}`); return val; } /** * Set the particular property of the device under test. * * @param prop - The name of the property. This name should * be known to _adb shell setprop_ tool. * @param val - The new property value. * @param opts * * @throws If _setprop_ utility fails to change property value. */ async function setDeviceProperty(prop, val, opts = {}) { const { privileged = true } = opts; logger_1.log.debug(`Setting device property '${prop}' to '${val}'`); await this.shell(['setprop', prop, val], { privileged, }); } /** * @returns Current system language on the device under test. */ async function getDeviceSysLanguage() { return await this.getDeviceProperty('persist.sys.language'); } /** * @returns Current country name on the device under test. */ async function getDeviceSysCountry() { return await this.getDeviceProperty('persist.sys.country'); } /** * @returns Current system locale name on the device under test. */ async function getDeviceSysLocale() { return await this.getDeviceProperty('persist.sys.locale'); } /** * @returns Current product language name on the device under test. */ async function getDeviceProductLanguage() { return await this.getDeviceProperty('ro.product.locale.language'); } /** * @returns Current product country name on the device under test. */ async function getDeviceProductCountry() { return await this.getDeviceProperty('ro.product.locale.region'); } /** * @returns Current product locale name on the device under test. */ async function getDeviceProductLocale() { return await this.getDeviceProperty('ro.product.locale'); } /** * @returns The model name of the device under test. */ async function getModel() { return await this.getDeviceProperty('ro.product.model'); } /** * @returns The manufacturer name of the device under test. */ async function getManufacturer() { return await this.getDeviceProperty('ro.product.manufacturer'); } /** * Get the current screen size. * * @returns Device screen size as string in format 'WxH' or * _null_ if it cannot be determined. */ async function getScreenSize() { const stdout = await this.shell(['wm', 'size']); const size = new RegExp(/Physical size: ([^\r?\n]+)*/g).exec(stdout); if (size && size.length >= 2) { return size[1].trim(); } return null; } /** * Get the current screen density in dpi * * @returns Device screen density as a number or _null_ if it * cannot be determined */ async function getScreenDensity() { const stdout = await this.shell(['wm', 'density']); const density = new RegExp(/Physical density: ([^\r?\n]+)*/g).exec(stdout); if (density && density.length >= 2) { const densityNumber = parseInt(density[1].trim(), 10); return isNaN(densityNumber) ? null : densityNumber; } return null; } /** * Setup HTTP proxy in device global settings. * Read https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r21/core/java/android/provider/Settings.java for each property * * @param proxyHost - The host name of the proxy. * @param proxyPort - The port number to be set. */ async function setHttpProxy(proxyHost, proxyPort) { const proxy = `${proxyHost}:${proxyPort}`; if (lodash_1.default.isUndefined(proxyHost)) { throw new Error(`Call to setHttpProxy method with undefined proxy_host: ${proxy}`); } if (lodash_1.default.isUndefined(proxyPort)) { throw new Error(`Call to setHttpProxy method with undefined proxy_port ${proxy}`); } /** @type {[string, string][]} */ const httpProxySettins = [ ['http_proxy', proxy], ['global_http_proxy_host', proxyHost], ['global_http_proxy_port', `${proxyPort}`], ]; for (const [settingKey, settingValue] of httpProxySettins) { await this.setSetting('global', settingKey, settingValue); } } /** * Delete HTTP proxy in device global settings. * Rebooting the test device is necessary to apply the change. */ async function deleteHttpProxy() { const httpProxySettins = [ 'http_proxy', 'global_http_proxy_host', 'global_http_proxy_port', 'global_http_proxy_exclusion_list', // `global_http_proxy_exclusion_list=` was generated by `settings global htto_proxy xxxx` ]; for (const setting of httpProxySettins) { await this.shell(['settings', 'delete', 'global', setting]); } } /** * Set device property. * [android.provider.Settings]{@link https://developer.android.com/reference/android/provider/Settings.html} * * @param namespace - one of {system, secure, global}, case-insensitive. * @param setting - property name. * @param value - property value. * @returns command output. */ async function setSetting(namespace, setting, value) { return await this.shell(['settings', 'put', namespace, setting, `${value}`]); } /** * Get device property. * [android.provider.Settings]{@link https://developer.android.com/reference/android/provider/Settings.html} * * @param namespace - one of {system, secure, global}, case-insensitive. * @param setting - property name. * @returns property value. */ async function getSetting(namespace, setting) { return await this.shell(['settings', 'get', namespace, setting]); } /** * Get tz database time zone formatted timezone * * @returns TZ database Time Zones format * @throws If any exception is reported by adb shell. */ async function getTimeZone() { logger_1.log.debug('Getting current timezone'); try { return await this.getDeviceProperty('persist.sys.timezone'); } catch (e) { const err = e; throw new Error(`Error getting timezone. Original error: ${err.message}`); } } /** * Retrieve the platform version of the device under test. * * @returns The platform version as a string, for example '5.0' for * Android Lollipop. */ async function getPlatformVersion() { logger_1.log.info('Getting device platform version'); try { return await this.getDeviceProperty('ro.build.version.release'); } catch (e) { const err = e; throw new Error(`Error getting device platform version. ` + `Original error: ${err.message}`); } } /** * Retrieve the list of location providers for the device under test. * * @returns The list of available location providers or an empty list. */ async function getLocationProviders() { if ((await this.getApiLevel()) < 31) { // https://stackoverflow.com/questions/70939503/settings-secure-location-providers-allowed-returns-null-in-android-12 const stdout = await this.getSetting('secure', 'location_providers_allowed'); return stdout .trim() .split(',') .map((p) => p.trim()) .filter(Boolean); } // To emulate the legacy behavior return lodash_1.default.includes(await this.shell(['cmd', 'location', 'is-location-enabled']), 'true') ? ['gps'] : []; } /** * Toggle the state of GPS location provider. * * @param enabled - Whether to enable (true) or disable (false) the GPS provider. */ async function toggleGPSLocationProvider(enabled) { if ((await this.getApiLevel()) < 31) { // https://stackoverflow.com/questions/70939503/settings-secure-location-providers-allowed-returns-null-in-android-12 await this.setSetting('secure', 'location_providers_allowed', `${enabled ? '+' : '-'}gps`); return; } await this.shell(['cmd', 'location', 'set-location-enabled', enabled ? 'true' : 'false']); } /** * Decorates an exception message with a solution link * * @param e The error object to be decorated * @returns Either the same error or the decorated one */ function decorateWriteSecureSettingsException(e) { if (lodash_1.default.includes(e.message, 'requires:android.permission.WRITE_SECURE_SETTINGS')) { e.message = `Check https://github.com/appium/appium/issues/13802 for throubleshooting. ${e.message}`; } return e; } /** * Set hidden api policy to manage access to non-SDK APIs. * https://developer.android.com/preview/restrictions-non-sdk-interfaces * * @param value - The API enforcement policy. * For Android P * 0: Disable non-SDK API usage detection. This will also disable logging, and also break the strict mode API, * detectNonSdkApiUsage(). Not recommended. * 1: "Just warn" - permit access to all non-SDK APIs, but keep warnings in the log. * The strict mode API will keep working. * 2: Disallow usage of dark grey and black listed APIs. * 3: Disallow usage of blacklisted APIs, but allow usage of dark grey listed APIs. * * For Android Q * https://developer.android.com/preview/non-sdk-q#enable-non-sdk-access * 0: Disable all detection of non-SDK interfaces. Using this setting disables all log messages for non-SDK interface usage * and prevents you from testing your app using the StrictMode API. This setting is not recommended. * 1: Enable access to all non-SDK interfaces, but print log messages with warnings for any non-SDK interface usage. * Using this setting also allows you to test your app using the StrictMode API. * 2: Disallow usage of non-SDK interfaces that belong to either the black list * or to a restricted greylist for your target API level. * * @param ignoreError - Whether to ignore an exception in 'adb shell settings put global' command * @throws If there was an error and ignoreError was true while executing 'adb shell settings put global' * command on the device under test. */ async function setHiddenApiPolicy(value, ignoreError = false) { try { await this.shell(HIDDEN_API_POLICY_KEYS.map((k) => `settings put global ${k} ${value}`).join(';')); } catch (e) { const err = e; if (!ignoreError) { throw decorateWriteSecureSettingsException(err); } logger_1.log.info(`Failed to set setting keys '${HIDDEN_API_POLICY_KEYS}' to '${value}'. ` + `Original error: ${err.message}`); } } /** * Reset access to non-SDK APIs to its default setting. * https://developer.android.com/preview/restrictions-non-sdk-interfaces * * @param ignoreError - Whether to ignore an exception in 'adb shell settings delete global' command * @throws If there was an error and ignoreError was true while executing 'adb shell settings delete global' * command on the device under test. */ async function setDefaultHiddenApiPolicy(ignoreError = false) { try { await this.shell(HIDDEN_API_POLICY_KEYS.map((k) => `settings delete global ${k}`).join(';')); } catch (e) { const err = e; if (!ignoreError) { throw decorateWriteSecureSettingsException(err); } logger_1.log.info(`Failed to delete keys '${HIDDEN_API_POLICY_KEYS}'. Original error: ${err.message}`); } } /** * Get the language name of the device under test. * * @returns The name of device language. */ async function getDeviceLanguage() { return (await this.getApiLevel()) < 23 ? (await this.getDeviceSysLanguage()) || (await this.getDeviceProductLanguage()) : (await this.getDeviceLocale()).split('-')[0]; } /** * Get the country name of the device under test. * * @summary Could only be used for Android API < 23 * @returns The name of device country. */ async function getDeviceCountry() { return (await this.getDeviceSysCountry()) || (await this.getDeviceProductCountry()); } /** * Get the locale name of the device under test. * * @summary Could only be used for Android API >= 23 * @returns The name of device locale. */ async function getDeviceLocale() { return (await this.getDeviceSysLocale()) || (await this.getDeviceProductLocale()); } /** * Make sure current device locale is expected or not. * * @privateRemarks FIXME: language or country is required * @param language - Language. The language field is case insensitive, but Locale always canonicalizes to lower case. * @param country - Country. The language field is case insensitive, but Locale always canonicalizes to lower case. * @param script - Script. The script field is case insensitive but Locale always canonicalizes to title case. * * @returns If current locale is language and country as arguments, return true. */ async function ensureCurrentLocale(language, country, script) { const hasLanguage = lodash_1.default.isString(language); const hasCountry = lodash_1.default.isString(country); if (!hasLanguage && !hasCountry) { logger_1.log.warn('ensureCurrentLocale requires language or country'); return false; } const lcLanguage = (language || '').toLowerCase(); const lcCountry = (country || '').toLowerCase(); const apiLevel = await this.getApiLevel(); return ((await (0, asyncbox_1.retryInterval)(5, 1000, async () => { if (apiLevel < 23) { logger_1.log.debug(`Requested locale: ${lcLanguage}-${lcCountry}`); let actualLanguage; if (hasLanguage) { actualLanguage = (await this.getDeviceLanguage()).toLowerCase(); logger_1.log.debug(`Actual language: ${actualLanguage}`); if (!hasCountry && lcLanguage === actualLanguage) { return true; } } let actualCountry; if (hasCountry) { actualCountry = (await this.getDeviceCountry()).toLowerCase(); logger_1.log.debug(`Actual country: ${actualCountry}`); if (!hasLanguage && lcCountry === actualCountry) { return true; } } return lcLanguage === actualLanguage && lcCountry === actualCountry; } const actualLocale = (await this.getDeviceLocale()).toLowerCase(); // zh-hans-cn : zh-cn const expectedLocale = script ? `${lcLanguage}-${script.toLowerCase()}-${lcCountry}` : `${lcLanguage}-${lcCountry}`; logger_1.log.debug(`Requested locale: ${expectedLocale}. Actual locale: '${actualLocale}'`); const languagePattern = `^${lodash_1.default.escapeRegExp(lcLanguage)}-${script ? lodash_1.default.escapeRegExp(script) + '-' : ''}`; const checkLocalePattern = (p) => new RegExp(p, 'i').test(actualLocale); if (hasLanguage && !hasCountry) { return checkLocalePattern(languagePattern); } const countryPattern = `${script ? '-' + lodash_1.default.escapeRegExp(script) : ''}-${lodash_1.default.escapeRegExp(lcCountry)}$`; if (!hasLanguage && hasCountry) { return checkLocalePattern(countryPattern); } return [languagePattern, countryPattern].every(checkLocalePattern); })) ?? false); } /** * Change the state of WiFi on the device under test. * Only works for real devices since API 30 * * @param on - True to enable and false to disable it. * @param isEmulator - Set it to true if the device under test * is an emulator rather than a real device. */ async function setWifiState(on, isEmulator = false) { if (isEmulator) { // The svc command does not require to be root since API 26 await this.shell(['svc', 'wifi', on ? 'enable' : 'disable'], { privileged: (await this.getApiLevel()) < 26, }); return; } await this.shell(['cmd', '-w', 'wifi', 'set-wifi-enabled', on ? 'enabled' : 'disabled']); } /** * Change the state of Data transfer on the device under test. * Only works for real devices since API 30 * * @param on - True to enable and false to disable it. * @param isEmulator - Set it to true if the device under test * is an emulator rather than a real device. */ async function setDataState(on, isEmulator = false) { if (isEmulator) { // The svc command does not require to be root since API 26 await this.shell(['svc', 'data', on ? 'enable' : 'disable'], { privileged: (await this.getApiLevel()) < 26, }); return; } await this.shell(['cmd', 'phone', 'data', on ? 'enable' : 'disable']); } /** * Retrieves the list of packages from Doze whitelist on Android 8+ * * @returns The list of whitelisted packages. An example output: * system,com.android.shell,2000 * system,com.google.android.cellbroadcastreceiver,10143 * user,io.appium.settings,10157 */ async function getDeviceIdleWhitelist() { if ((await this.getApiLevel()) < 23) { // Doze mode has only been added since Android 6 return []; } logger_1.log.info('Listing packages in Doze whitelist'); const output = await this.shell(['dumpsys', 'deviceidle', 'whitelist']); return lodash_1.default.trim(output) .split(/\n/) .map((line) => lodash_1.default.trim(line)) .filter(Boolean); } /** * Adds an existing package(s) into the Doze whitelist on Android 8+ * * @param packages One or more packages to add. If the package * already exists in the whitelist then it is only going to be added once. * If the package with the given name is not installed/not known then an error * will be thrown. * @returns `true` if the command to add package(s) has been executed */ async function addToDeviceIdleWhitelist(...packages) { if (lodash_1.default.isEmpty(packages) || (await this.getApiLevel()) < 23) { // Doze mode has only been added since Android 6 return false; } logger_1.log.info(`Adding ${support_1.util.pluralize('package', packages.length)} ${JSON.stringify(packages)} to Doze whitelist`); await this.shellChunks((pkg) => ['dumpsys', 'deviceidle', 'whitelist', `+${pkg}`], packages); return true; } /** * Check the state of Airplane mode on the device under test. * * @returns True if Airplane mode is enabled. */ async function isAirplaneModeOn() { const stdout = await this.getSetting('global', 'airplane_mode_on'); return parseInt(stdout, 10) !== 0; // Alternatively for Android 11+: // return (await this.shell(['cmd', 'connectivity', 'airplane-mode'])).stdout.trim() === 'enabled'; } /** * Change the state of Airplane mode in Settings on the device under test. * * @param on - True to enable the Airplane mode in Settings and false to disable it. */ async function setAirplaneMode(on) { if ((await this.getApiLevel()) < 30) { // This requires to call broadcastAirplaneMode afterwards to apply await this.setSetting('global', 'airplane_mode_on', on ? 1 : 0); return; } await this.shell(['cmd', 'connectivity', 'airplane-mode', on ? 'enable' : 'disable']); } /** * Change the state of the bluetooth service on the device under test. * * @param on - True to enable bluetooth service and false to disable it. */ async function setBluetoothOn(on) { if ((await this.getApiLevel()) < 30) { throw new Error('Changing of the bluetooth state is not supported on your device'); } await this.shell(['cmd', 'bluetooth_manager', on ? 'enable' : 'disable']); } /** * Change the state of the NFC service on the device under test. * * @param on - True to enable NFC service and false to disable it. * @throws If there was an error while changing the service state */ async function setNfcOn(on) { const { stdout, stderr } = (await this.shell(['svc', 'nfc', on ? 'enable' : 'disable'], { outputFormat: 'full', })); const output = stderr || stdout; logger_1.log.debug(output); if (output.includes('null NfcAdapter')) { throw new Error(`Cannot turn ${on ? 'on' : 'off'} the NFC adapter. Does the device under test have it?`); } } /** * Broadcast the state of Airplane mode on the device under test. * This method should be called after {@link #setAirplaneMode}, otherwise * the mode change is not going to be applied for the device. * ! This API requires root since Android API 24. Since API 30 * there is a dedicated adb command to change airplane mode state, which * does not require to call this one afterwards. * * @param on - True to broadcast enable and false to broadcast disable. */ async function broadcastAirplaneMode(on) { const args = [ 'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez', 'state', on ? 'true' : 'false', ]; try { await this.shell(args); } catch (e) { const err = e; // https://github.com/appium/appium/issues/17422 if (lodash_1.default.includes(err.stderr, 'SecurityException')) { try { await this.shell(args, { privileged: true }); return; } catch { } } throw err; } } /** * Check the state of WiFi on the device under test. * * @returns True if WiFi is enabled. */ async function isWifiOn() { const stdout = await this.getSetting('global', 'wifi_on'); return parseInt(stdout, 10) !== 0; // Alternative for Android 11+: // return (await this.shell(['cmd', 'wifi', 'status']).stdout.includes('Wifi is enabled')); } /** * Check the state of Data transfer on the device under test. * * @returns True if Data transfer is enabled. */ async function isDataOn() { const stdout = await this.getSetting('global', 'mobile_data'); return parseInt(stdout, 10) !== 0; } /** * Check the state of animation on the device under test below: * - animator_duration_scale * - transition_animation_scale * - window_animation_scale * * @returns True if at least one of animation scale settings * is not equal to '0.0'. */ async function isAnimationOn() { return (await bluebird_1.default.all(ANIMATION_SCALE_KEYS.map(async (k) => (await this.getSetting('global', k)) !== '0.0'))).includes(true); } /** * Set animation scale with the given value via adb shell settings command. * - animator_duration_scale * - transition_animation_scale * - window_animation_scale * API level 24 and newer OS versions may change the animation, at least emulators are so. * API level 28+ real devices checked this worked, but we haven't checked older ones * with real devices. * * @param value Animation scale value (int or float) to set. * The minimum value of zero disables animations. * By increasing the value, animations become slower. * '1' is the system default animation scale. * @throws If the adb setting command raises an exception. */ async function setAnimationScale(value) { await bluebird_1.default.all(ANIMATION_SCALE_KEYS.map((k) => this.setSetting('global', k, value))); } /** * Retrieve current screen orientation of the device under test. * * @returns The current orientation encoded as an integer number. */ async function getScreenOrientation() { const stdout = await this.shell(['dumpsys', 'input']); return getSurfaceOrientation(stdout); } // #region Private functions /** * Reads SurfaceOrientation in dumpsys output * * @param dumpsys */ function getSurfaceOrientation(dumpsys) { const m = /SurfaceOrientation: \d/gi.exec(dumpsys); return m ? parseInt(m[0].split(':')[1], 10) : null; } // #endregion //# sourceMappingURL=device-settings.js.map