io.appium.settings 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/apks/settings_apk-debug.apk +0 -0
- package/build/lib/client.d.ts +83 -0
- package/build/lib/client.d.ts.map +1 -0
- package/build/lib/client.js +137 -0
- package/build/lib/client.js.map +1 -0
- package/build/lib/commands/animation.d.ts +16 -0
- package/build/lib/commands/animation.d.ts.map +1 -0
- package/build/lib/commands/animation.js +29 -0
- package/build/lib/commands/animation.js.map +1 -0
- package/build/lib/commands/clipboard.d.ts +16 -0
- package/build/lib/commands/clipboard.d.ts.map +1 -0
- package/build/lib/commands/clipboard.js +51 -0
- package/build/lib/commands/clipboard.js.map +1 -0
- package/build/lib/commands/geolocation.d.ts +70 -0
- package/build/lib/commands/geolocation.d.ts.map +1 -0
- package/build/lib/commands/geolocation.js +215 -0
- package/build/lib/commands/geolocation.js.map +1 -0
- package/build/lib/commands/locale.d.ts +13 -0
- package/build/lib/commands/locale.d.ts.map +1 -0
- package/build/lib/commands/locale.js +31 -0
- package/build/lib/commands/locale.js.map +1 -0
- package/build/lib/commands/media.d.ts +11 -0
- package/build/lib/commands/media.d.ts.map +1 -0
- package/build/lib/commands/media.js +35 -0
- package/build/lib/commands/media.js.map +1 -0
- package/build/lib/commands/network.d.ts +19 -0
- package/build/lib/commands/network.d.ts.map +1 -0
- package/build/lib/commands/network.js +68 -0
- package/build/lib/commands/network.js.map +1 -0
- package/build/lib/commands/notifications.d.ts +50 -0
- package/build/lib/commands/notifications.d.ts.map +1 -0
- package/build/lib/commands/notifications.js +77 -0
- package/build/lib/commands/notifications.js.map +1 -0
- package/build/lib/commands/sms.d.ts +90 -0
- package/build/lib/commands/sms.d.ts.map +1 -0
- package/build/lib/commands/sms.js +95 -0
- package/build/lib/commands/sms.js.map +1 -0
- package/build/lib/commands/typing.d.ts +23 -0
- package/build/lib/commands/typing.d.ts.map +1 -0
- package/build/lib/commands/typing.js +51 -0
- package/build/lib/commands/typing.js.map +1 -0
- package/build/lib/commands/utf7.d.ts +26 -0
- package/build/lib/commands/utf7.d.ts.map +1 -0
- package/build/lib/commands/utf7.js +144 -0
- package/build/lib/commands/utf7.js.map +1 -0
- package/build/lib/constants.d.ts +23 -0
- package/build/lib/constants.d.ts.map +1 -0
- package/build/lib/constants.js +26 -0
- package/build/lib/constants.js.map +1 -0
- package/build/lib/logger.d.ts +3 -0
- package/build/lib/logger.d.ts.map +1 -0
- package/build/lib/logger.js +17 -0
- package/build/lib/logger.js.map +1 -0
- package/index.js +6 -2
- package/lib/client.js +155 -0
- package/lib/commands/animation.js +24 -0
- package/lib/commands/clipboard.js +47 -0
- package/lib/commands/geolocation.js +214 -0
- package/lib/commands/locale.js +28 -0
- package/lib/commands/media.js +27 -0
- package/lib/commands/network.js +73 -0
- package/lib/commands/notifications.js +71 -0
- package/lib/commands/sms.js +92 -0
- package/lib/commands/typing.js +46 -0
- package/lib/commands/utf7.js +154 -0
- package/lib/constants.js +31 -0
- package/lib/logger.js +14 -0
- package/package.json +44 -8
package/lib/client.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { log, LOG_PREFIX } from './logger';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import { waitForCondition } from 'asyncbox';
|
|
4
|
+
import { SETTINGS_HELPER_ID, SETTINGS_HELPER_MAIN_ACTIVITY } from './constants.js';
|
|
5
|
+
import { setAnimationState } from './commands/animation';
|
|
6
|
+
import { getClipboard } from './commands/clipboard';
|
|
7
|
+
import { setGeoLocation, getGeoLocation, refreshGeoLocationCache } from './commands/geolocation';
|
|
8
|
+
import { setDeviceSysLocale } from './commands/locale';
|
|
9
|
+
import { scanMedia } from './commands/media';
|
|
10
|
+
import { setDataState, setWifiState } from './commands/network';
|
|
11
|
+
import { getNotifications } from './commands/notifications';
|
|
12
|
+
import { getSmsList } from './commands/sms';
|
|
13
|
+
import { performEditorAction, typeUnicode } from './commands/typing';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} SettingsAppOpts
|
|
17
|
+
* @property {import('appium-adb').ADB} adb
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export class SettingsApp {
|
|
22
|
+
/** @type {import('appium-adb').ADB} */
|
|
23
|
+
adb;
|
|
24
|
+
|
|
25
|
+
/** @type {import('npmlog').Logger} */
|
|
26
|
+
log;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {SettingsAppOpts} opts
|
|
30
|
+
*/
|
|
31
|
+
constructor (opts) {
|
|
32
|
+
this.adb = opts.adb;
|
|
33
|
+
this.log = log;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} SettingsAppStartupOptions
|
|
38
|
+
* @property {number} [timeout=5000] The maximum number of milliseconds
|
|
39
|
+
* to wait until the app has started
|
|
40
|
+
* @property {boolean} [shouldRestoreCurrentApp=false] Whether to restore
|
|
41
|
+
* the activity which was the current one before Settings startup
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ensures that Appium Settings helper application is running
|
|
46
|
+
* and starts it if necessary
|
|
47
|
+
*
|
|
48
|
+
* @param {SettingsAppStartupOptions} [opts={}]
|
|
49
|
+
* @throws {Error} If Appium Settings has failed to start
|
|
50
|
+
* @returns {Promise<SettingsApp>} self instance for chaining
|
|
51
|
+
*/
|
|
52
|
+
async requireRunning (opts = {}) {
|
|
53
|
+
if (await this.adb.processExists(SETTINGS_HELPER_ID)) {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.log.debug(LOG_PREFIX, 'Starting Appium Settings app');
|
|
58
|
+
const {
|
|
59
|
+
timeout = 5000,
|
|
60
|
+
shouldRestoreCurrentApp = false,
|
|
61
|
+
} = opts;
|
|
62
|
+
let appPackage;
|
|
63
|
+
if (shouldRestoreCurrentApp) {
|
|
64
|
+
try {
|
|
65
|
+
({appPackage} = await this.adb.getFocusedPackageAndActivity());
|
|
66
|
+
} catch (e) {
|
|
67
|
+
this.log.warn(LOG_PREFIX, `The current application can not be restored: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await this.adb.startApp({
|
|
71
|
+
pkg: SETTINGS_HELPER_ID,
|
|
72
|
+
activity: SETTINGS_HELPER_MAIN_ACTIVITY,
|
|
73
|
+
action: 'android.intent.action.MAIN',
|
|
74
|
+
category: 'android.intent.category.LAUNCHER',
|
|
75
|
+
stopApp: false,
|
|
76
|
+
waitForLaunch: false,
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
await waitForCondition(async () => await this.adb.processExists(SETTINGS_HELPER_ID), {
|
|
80
|
+
waitMs: timeout,
|
|
81
|
+
intervalMs: 300,
|
|
82
|
+
});
|
|
83
|
+
if (shouldRestoreCurrentApp && appPackage) {
|
|
84
|
+
try {
|
|
85
|
+
await this.adb.activateApp(appPackage);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
log.warn(`The current application can not be restored: ${e.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return this;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(`Appium Settings app is not running after ${timeout}ms`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parses the output in JSON format retrieved from
|
|
98
|
+
* the corresponding Appium Settings broadcast calls
|
|
99
|
+
*
|
|
100
|
+
* @param {string} output The actual command output
|
|
101
|
+
* @param {string} entityName The name of the entity which is
|
|
102
|
+
* going to be parsed
|
|
103
|
+
* @returns {Object} The parsed JSON object
|
|
104
|
+
* @throws {Error} If the output cannot be parsed
|
|
105
|
+
* as a valid JSON
|
|
106
|
+
*/
|
|
107
|
+
_parseJsonData (output, entityName) {
|
|
108
|
+
if (!/\bresult=-1\b/.test(output) || !/\bdata="/.test(output)) {
|
|
109
|
+
this.log.debug(LOG_PREFIX, output);
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Cannot retrieve ${entityName} from the device. ` +
|
|
112
|
+
'Check the server log for more details'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const match = /\bdata="(.+)",?/.exec(output);
|
|
116
|
+
if (!match) {
|
|
117
|
+
this.log.debug(LOG_PREFIX, output);
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Cannot parse ${entityName} from the command output. ` +
|
|
120
|
+
'Check the server log for more details'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const jsonStr = _.trim(match[1]);
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(jsonStr);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
log.debug(jsonStr);
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Cannot parse ${entityName} from the resulting data string. ` +
|
|
130
|
+
'Check the server log for more details'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setAnimationState = setAnimationState;
|
|
136
|
+
|
|
137
|
+
getClipboard = getClipboard;
|
|
138
|
+
|
|
139
|
+
setGeoLocation = setGeoLocation;
|
|
140
|
+
getGeoLocation = getGeoLocation;
|
|
141
|
+
refreshGeoLocationCache = refreshGeoLocationCache;
|
|
142
|
+
|
|
143
|
+
setDeviceSysLocale = setDeviceSysLocale;
|
|
144
|
+
|
|
145
|
+
scanMedia = scanMedia;
|
|
146
|
+
|
|
147
|
+
setDataState = setDataState;
|
|
148
|
+
setWifiState = setWifiState;
|
|
149
|
+
|
|
150
|
+
getNotifications = getNotifications;
|
|
151
|
+
getSmsList = getSmsList;
|
|
152
|
+
|
|
153
|
+
performEditorAction = performEditorAction;
|
|
154
|
+
typeUnicode = typeUnicode;
|
|
155
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ANIMATION_SETTING_ACTION, ANIMATION_SETTING_RECEIVER } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change the state of animation on the device under test.
|
|
5
|
+
* Animation on the device is controlled by the following global properties:
|
|
6
|
+
* [ANIMATOR_DURATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#ANIMATOR_DURATION_SCALE},
|
|
7
|
+
* [TRANSITION_ANIMATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#TRANSITION_ANIMATION_SCALE},
|
|
8
|
+
* [WINDOW_ANIMATION_SCALE]{@link https://developer.android.com/reference/android/provider/Settings.Global.html#WINDOW_ANIMATION_SCALE}.
|
|
9
|
+
* This method sets all this properties to 0.0 to disable (1.0 to enable) animation.
|
|
10
|
+
*
|
|
11
|
+
* Turning off animation might be useful to improve stability
|
|
12
|
+
* and reduce tests execution time.
|
|
13
|
+
*
|
|
14
|
+
* @this {import('../client').SettingsApp}
|
|
15
|
+
* @param {boolean} on - True to enable and false to disable it.
|
|
16
|
+
*/
|
|
17
|
+
export async function setAnimationState (on) {
|
|
18
|
+
await this.adb.shell([
|
|
19
|
+
'am', 'broadcast',
|
|
20
|
+
'-a', ANIMATION_SETTING_ACTION,
|
|
21
|
+
'-n', ANIMATION_SETTING_RECEIVER,
|
|
22
|
+
'--es', 'setstatus', on ? 'enable' : 'disable'
|
|
23
|
+
]);
|
|
24
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { LOG_PREFIX } from '../logger';
|
|
3
|
+
import {
|
|
4
|
+
CLIPBOARD_RECEIVER,
|
|
5
|
+
CLIPBOARD_RETRIEVAL_ACTION,
|
|
6
|
+
APPIUM_IME
|
|
7
|
+
} from '../constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the text content of the device's clipboard.
|
|
11
|
+
* The method works for Android below and above 29.
|
|
12
|
+
* It temorarily enforces the IME setting in order to workaround
|
|
13
|
+
* security limitations if needed.
|
|
14
|
+
* This method only works if Appium Settings v. 2.15+ is installed
|
|
15
|
+
* on the device under test
|
|
16
|
+
*
|
|
17
|
+
* @this {import('../client').SettingsApp}
|
|
18
|
+
* @returns {Promise<string>} The actual content of the main clipboard as
|
|
19
|
+
* base64-encoded string or an empty string if the clipboard is empty
|
|
20
|
+
* @throws {Error} If there was a problem while getting the
|
|
21
|
+
* clipboard contant
|
|
22
|
+
*/
|
|
23
|
+
export async function getClipboard () {
|
|
24
|
+
this.log.debug(LOG_PREFIX, 'Getting the clipboard content');
|
|
25
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
26
|
+
const retrieveClipboard = async () => await this.adb.shell([
|
|
27
|
+
'am', 'broadcast',
|
|
28
|
+
'-n', CLIPBOARD_RECEIVER,
|
|
29
|
+
'-a', CLIPBOARD_RETRIEVAL_ACTION,
|
|
30
|
+
]);
|
|
31
|
+
let output;
|
|
32
|
+
try {
|
|
33
|
+
output = (await this.adb.getApiLevel() >= 29)
|
|
34
|
+
? (await this.adb.runInImeContext(APPIUM_IME, retrieveClipboard))
|
|
35
|
+
: (await retrieveClipboard());
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(`Cannot retrieve the current clipboard content from the device. ` +
|
|
38
|
+
`Make sure the Appium Settings application is up to date. ` +
|
|
39
|
+
`Original error: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const match = /data="([^"]*)"/.exec(output);
|
|
43
|
+
if (!match) {
|
|
44
|
+
throw new Error(`Cannot parse the actual cliboard content from the command output: ${output}`);
|
|
45
|
+
}
|
|
46
|
+
return _.trim(match[1]);
|
|
47
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import {
|
|
3
|
+
LOCATION_SERVICE,
|
|
4
|
+
LOCATION_RECEIVER,
|
|
5
|
+
LOCATION_RETRIEVAL_ACTION,
|
|
6
|
+
} from '../constants.js';
|
|
7
|
+
import { SubProcess } from 'teen_process';
|
|
8
|
+
import B from 'bluebird';
|
|
9
|
+
import { LOG_PREFIX } from '../logger.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SATELLITES_COUNT = 12;
|
|
12
|
+
const DEFAULT_ALTITUDE = 0.0;
|
|
13
|
+
const LOCATION_TRACKER_TAG = 'LocationTracker';
|
|
14
|
+
const GPS_CACHE_REFRESHED_LOGS = [
|
|
15
|
+
'The current location has been successfully retrieved from Play Services',
|
|
16
|
+
'The current location has been successfully retrieved from Location Manager'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const GPS_COORDINATES_PATTERN = /data="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)"/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} Location
|
|
23
|
+
* @property {number|string} longitude - Valid longitude value.
|
|
24
|
+
* @property {number|string} latitude - Valid latitude value.
|
|
25
|
+
* @property {?number|string} [altitude] - Valid altitude value.
|
|
26
|
+
* @property {?number|string} [satellites=12] - Number of satellites being tracked (1-12).
|
|
27
|
+
* This value is ignored on real devices.
|
|
28
|
+
* @property {?number|string} [speed] - Valid speed value.
|
|
29
|
+
* Should be greater than 0.0 meters/second for real devices or 0.0 knots
|
|
30
|
+
* for emulators.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Emulate geolocation coordinates on the device under test.
|
|
35
|
+
*
|
|
36
|
+
* @this {import('../client').SettingsApp}
|
|
37
|
+
* @param {Location} location - Location object. The `altitude` value is ignored
|
|
38
|
+
* while mocking the position.
|
|
39
|
+
* @param {boolean} [isEmulator=false] - Set it to true if the device under test
|
|
40
|
+
* is an emulator rather than a real device.
|
|
41
|
+
*/
|
|
42
|
+
export async function setGeoLocation (location, isEmulator = false) {
|
|
43
|
+
const formatLocationValue = (valueName, isRequired = true) => {
|
|
44
|
+
if (_.isNil(location[valueName])) {
|
|
45
|
+
if (isRequired) {
|
|
46
|
+
throw new Error(`${valueName} must be provided`);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const floatValue = parseFloat(location[valueName]);
|
|
51
|
+
if (!isNaN(floatValue)) {
|
|
52
|
+
return `${_.ceil(floatValue, 5)}`;
|
|
53
|
+
}
|
|
54
|
+
if (isRequired) {
|
|
55
|
+
throw new Error(`${valueName} is expected to be a valid float number. ` +
|
|
56
|
+
`'${location[valueName]}' is given instead`);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
const longitude = /** @type {string} */ (formatLocationValue('longitude'));
|
|
61
|
+
const latitude = /** @type {string} */ (formatLocationValue('latitude'));
|
|
62
|
+
const altitude = formatLocationValue('altitude', false);
|
|
63
|
+
const speed = formatLocationValue('speed', false);
|
|
64
|
+
if (isEmulator) {
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const args = [longitude, latitude];
|
|
67
|
+
if (!_.isNil(altitude)) {
|
|
68
|
+
args.push(altitude);
|
|
69
|
+
}
|
|
70
|
+
const satellites = parseInt(`${location.satellites}`, 10);
|
|
71
|
+
if (!Number.isNaN(satellites) && satellites > 0 && satellites <= 12) {
|
|
72
|
+
if (args.length < 3) {
|
|
73
|
+
args.push(`${DEFAULT_ALTITUDE}`);
|
|
74
|
+
}
|
|
75
|
+
args.push(`${satellites}`);
|
|
76
|
+
}
|
|
77
|
+
if (!_.isNil(speed)) {
|
|
78
|
+
if (args.length < 3) {
|
|
79
|
+
args.push(`${DEFAULT_ALTITUDE}`);
|
|
80
|
+
}
|
|
81
|
+
if (args.length < 4) {
|
|
82
|
+
args.push(`${DEFAULT_SATELLITES_COUNT}`);
|
|
83
|
+
}
|
|
84
|
+
args.push(speed);
|
|
85
|
+
}
|
|
86
|
+
await this.adb.resetTelnetAuthToken();
|
|
87
|
+
await this.adb.adbExec(['emu', 'geo', 'fix', ...args]);
|
|
88
|
+
// A workaround for https://code.google.com/p/android/issues/detail?id=206180
|
|
89
|
+
await this.adb.adbExec(['emu', 'geo', 'fix', ...(args.map((arg) => arg.replace('.', ',')))]);
|
|
90
|
+
} else {
|
|
91
|
+
const args = [
|
|
92
|
+
'am', (await this.adb.getApiLevel() >= 26) ? 'start-foreground-service' : 'startservice',
|
|
93
|
+
'-e', 'longitude', longitude,
|
|
94
|
+
'-e', 'latitude', latitude,
|
|
95
|
+
];
|
|
96
|
+
if (!_.isNil(altitude)) {
|
|
97
|
+
args.push('-e', 'altitude', altitude);
|
|
98
|
+
}
|
|
99
|
+
if (!_.isNil(speed)) {
|
|
100
|
+
args.push('-e', 'speed', speed);
|
|
101
|
+
}
|
|
102
|
+
args.push(LOCATION_SERVICE);
|
|
103
|
+
await this.adb.shell(args);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the current cached GPS location from the device under test.
|
|
109
|
+
*
|
|
110
|
+
* @this {import('../client').SettingsApp}
|
|
111
|
+
* @returns {Promise<Location>} The current location
|
|
112
|
+
* @throws {Error} If the current location cannot be retrieved
|
|
113
|
+
*/
|
|
114
|
+
export async function getGeoLocation () {
|
|
115
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
116
|
+
|
|
117
|
+
let output;
|
|
118
|
+
try {
|
|
119
|
+
output = await this.adb.shell([
|
|
120
|
+
'am', 'broadcast',
|
|
121
|
+
'-n', LOCATION_RECEIVER,
|
|
122
|
+
'-a', LOCATION_RETRIEVAL_ACTION,
|
|
123
|
+
]);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
throw new Error(`Cannot retrieve the current geo coordinates from the device. ` +
|
|
126
|
+
`Make sure the Appium Settings application is up to date and has location permissions. Also the location ` +
|
|
127
|
+
`services must be enabled on the device. Original error: ${err.stderr || err.stdout || err.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const match = GPS_COORDINATES_PATTERN.exec(output);
|
|
131
|
+
if (!match) {
|
|
132
|
+
throw new Error(`Cannot parse the actual location values from the command output: ${output}`);
|
|
133
|
+
}
|
|
134
|
+
const location = {
|
|
135
|
+
latitude: match[1],
|
|
136
|
+
longitude: match[2],
|
|
137
|
+
altitude: match[3],
|
|
138
|
+
};
|
|
139
|
+
this.log.debug(LOG_PREFIX, `Got geo coordinates: ${JSON.stringify(location)}`);
|
|
140
|
+
return location;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sends an async request to refresh the GPS cache.
|
|
145
|
+
* This feature only works if the device under test has
|
|
146
|
+
* Google Play Services installed. In case the vanilla
|
|
147
|
+
* LocationManager is used the device API level must be at
|
|
148
|
+
* version 30 (Android R) or higher.
|
|
149
|
+
*
|
|
150
|
+
* @this {import('../client').SettingsApp}
|
|
151
|
+
* @param {number} timeoutMs The maximum number of milliseconds
|
|
152
|
+
* to block until GPS cache is refreshed. Providing zero or a negative
|
|
153
|
+
* value to it skips waiting completely.
|
|
154
|
+
*
|
|
155
|
+
* @throws {Error} If the GPS cache cannot be refreshed.
|
|
156
|
+
*/
|
|
157
|
+
export async function refreshGeoLocationCache (timeoutMs = 20000) {
|
|
158
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
159
|
+
|
|
160
|
+
let logcatMonitor;
|
|
161
|
+
let monitoringPromise;
|
|
162
|
+
|
|
163
|
+
if (timeoutMs > 0) {
|
|
164
|
+
const cmd = [
|
|
165
|
+
...this.adb.executable.defaultArgs,
|
|
166
|
+
'logcat', '-s', LOCATION_TRACKER_TAG,
|
|
167
|
+
];
|
|
168
|
+
logcatMonitor = new SubProcess(this.adb.executable.path, cmd);
|
|
169
|
+
const timeoutErrorMsg = `The GPS cache has not been refreshed within ${timeoutMs}ms timeout. ` +
|
|
170
|
+
`Please make sure the device under test has Appium Settings app installed and running. ` +
|
|
171
|
+
`Also, it is required that the device has Google Play Services installed or is running ` +
|
|
172
|
+
`Android 10+ otherwise.`;
|
|
173
|
+
monitoringPromise = new B((resolve, reject) => {
|
|
174
|
+
setTimeout(() => reject(new Error(timeoutErrorMsg)), timeoutMs);
|
|
175
|
+
|
|
176
|
+
logcatMonitor.on('exit', () => reject(new Error(timeoutErrorMsg)));
|
|
177
|
+
['lines-stderr', 'lines-stdout'].map((evt) => logcatMonitor.on(evt, (lines) => {
|
|
178
|
+
if (lines.some((line) => GPS_CACHE_REFRESHED_LOGS.some((x) => line.includes(x)))) {
|
|
179
|
+
resolve();
|
|
180
|
+
}
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
await logcatMonitor.start(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await this.adb.shell([
|
|
188
|
+
'am', 'broadcast',
|
|
189
|
+
'-n', LOCATION_RECEIVER,
|
|
190
|
+
'-a', LOCATION_RETRIEVAL_ACTION,
|
|
191
|
+
'--ez', 'forceUpdate', 'true',
|
|
192
|
+
]);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new Error(`Cannot refresh the GPS cache on the device. ` +
|
|
195
|
+
`Make sure the Appium Settings application is up to date and has location permissions. Also the location ` +
|
|
196
|
+
`services must be enabled on the device. Original error: ${err.stderr || err.stdout || err.message}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (logcatMonitor && monitoringPromise) {
|
|
200
|
+
const startMs = performance.now();
|
|
201
|
+
this.log.debug(LOG_PREFIX, `Waiting up to ${timeoutMs}ms for the GPS cache to be refreshed`);
|
|
202
|
+
try {
|
|
203
|
+
await monitoringPromise;
|
|
204
|
+
this.log.info(LOG_PREFIX, `The GPS cache has been successfully refreshed after ` +
|
|
205
|
+
`${(performance.now() - startMs).toFixed(0)}ms`);
|
|
206
|
+
} finally {
|
|
207
|
+
if (logcatMonitor.isRunning) {
|
|
208
|
+
await logcatMonitor.stop();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
this.log.info(LOG_PREFIX, 'The request to refresh the GPS cache has been sent. Skipping waiting for its result.');
|
|
213
|
+
}
|
|
214
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LOCALE_SETTING_ACTION, LOCALE_SETTING_RECEIVER } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change the locale on the device under test. Don't need to reboot the device after changing the locale.
|
|
5
|
+
* This method sets an arbitrary locale following:
|
|
6
|
+
* https://developer.android.com/reference/java/util/Locale.html
|
|
7
|
+
* https://developer.android.com/reference/java/util/Locale.html#Locale(java.lang.String,%20java.lang.String)
|
|
8
|
+
*
|
|
9
|
+
* @this {import('../client').SettingsApp}
|
|
10
|
+
* @param {string} language - Language. e.g. en, ja
|
|
11
|
+
* @param {string} country - Country. e.g. US, JP
|
|
12
|
+
* @param {string?} [script=null] - Script. e.g. Hans in `zh-Hans-CN`
|
|
13
|
+
*/
|
|
14
|
+
export async function setDeviceSysLocale (language, country, script = null) {
|
|
15
|
+
const params = [
|
|
16
|
+
'am', 'broadcast',
|
|
17
|
+
'-a', LOCALE_SETTING_ACTION,
|
|
18
|
+
'-n', LOCALE_SETTING_RECEIVER,
|
|
19
|
+
'--es', 'lang', language.toLowerCase(),
|
|
20
|
+
'--es', 'country', country.toUpperCase()
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (script) {
|
|
24
|
+
params.push('--es', 'script', script);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await this.adb.shell(params);
|
|
28
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { LOG_PREFIX } from '../logger.js';
|
|
3
|
+
import { MEDIA_SCAN_ACTION, MEDIA_SCAN_RECEIVER } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Performs recursive media scan at the given destination.
|
|
7
|
+
* All successfully scanned items are being added to the device's
|
|
8
|
+
* media library.
|
|
9
|
+
*
|
|
10
|
+
* @this {import('../client').SettingsApp}
|
|
11
|
+
* @param {string} destination File/folder path on the remote device.
|
|
12
|
+
* @throws {Error} If there was an unexpected error by scanning.
|
|
13
|
+
*/
|
|
14
|
+
export async function scanMedia (destination) {
|
|
15
|
+
this.log.debug(LOG_PREFIX, `Scanning '${destination}' for media files`);
|
|
16
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
17
|
+
const output = await this.adb.shell([
|
|
18
|
+
'am', 'broadcast',
|
|
19
|
+
'-n', MEDIA_SCAN_RECEIVER,
|
|
20
|
+
'-a', MEDIA_SCAN_ACTION,
|
|
21
|
+
'--es', 'path', destination
|
|
22
|
+
]);
|
|
23
|
+
if (!_.includes(output, 'result=-1')) {
|
|
24
|
+
throw new Error(`No media could be scanned at '${destination}'. ` +
|
|
25
|
+
`Check the device logcat output for more details.`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WIFI_CONNECTION_SETTING_ACTION,
|
|
3
|
+
WIFI_CONNECTION_SETTING_RECEIVER,
|
|
4
|
+
DATA_CONNECTION_SETTING_ACTION,
|
|
5
|
+
DATA_CONNECTION_SETTING_RECEIVER,
|
|
6
|
+
} from '../constants.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Change the state of WiFi on the device under test.
|
|
10
|
+
*
|
|
11
|
+
* @this {import('../client').SettingsApp}
|
|
12
|
+
* @param {boolean} on - True to enable and false to disable it.
|
|
13
|
+
* @param {boolean} [isEmulator=false] - Set it to true if the device under test
|
|
14
|
+
* is an emulator rather than a real device.
|
|
15
|
+
*/
|
|
16
|
+
export async function setWifiState (on, isEmulator = false) {
|
|
17
|
+
if (isEmulator) {
|
|
18
|
+
// The svc command does not require to be root since API 26
|
|
19
|
+
await this.adb.shell(['svc', 'wifi', on ? 'enable' : 'disable'], {
|
|
20
|
+
privileged: await this.adb.getApiLevel() < 26,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (await this.adb.getApiLevel() < 30) {
|
|
26
|
+
// Android below API 30 does not have a dedicated adb command
|
|
27
|
+
// to manipulate wifi connection state, so try to do it via Settings app
|
|
28
|
+
// as a workaround
|
|
29
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
30
|
+
|
|
31
|
+
await this.adb.shell([
|
|
32
|
+
'am', 'broadcast',
|
|
33
|
+
'-a', WIFI_CONNECTION_SETTING_ACTION,
|
|
34
|
+
'-n', WIFI_CONNECTION_SETTING_RECEIVER,
|
|
35
|
+
'--es', 'setstatus', on ? 'enable' : 'disable'
|
|
36
|
+
]);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await this.adb.shell(['cmd', '-w', 'wifi', 'set-wifi-enabled', on ? 'enabled' : 'disabled']);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Change the state of Data transfer on the device under test.
|
|
45
|
+
*
|
|
46
|
+
* @this {import('../client').SettingsApp}
|
|
47
|
+
* @param {boolean} on - True to enable and false to disable it.
|
|
48
|
+
* @param {boolean} [isEmulator=false] - Set it to true if the device under test
|
|
49
|
+
* is an emulator rather than a real device.
|
|
50
|
+
*/
|
|
51
|
+
export async function setDataState (on, isEmulator = false) {
|
|
52
|
+
if (isEmulator) {
|
|
53
|
+
// The svc command does not require to be root since API 26
|
|
54
|
+
await this.adb.shell(['svc', 'data', on ? 'enable' : 'disable'], {
|
|
55
|
+
privileged: await this.adb.getApiLevel() < 26,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (await this.adb.getApiLevel() < 30) {
|
|
61
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
62
|
+
|
|
63
|
+
await this.adb.shell([
|
|
64
|
+
'am', 'broadcast',
|
|
65
|
+
'-a', DATA_CONNECTION_SETTING_ACTION,
|
|
66
|
+
'-n', DATA_CONNECTION_SETTING_RECEIVER,
|
|
67
|
+
'--es', 'setstatus', on ? 'enable' : 'disable'
|
|
68
|
+
]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this.adb.shell(['cmd', 'phone', 'data', on ? 'enable' : 'disable']);
|
|
73
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LOG_PREFIX } from '../logger';
|
|
2
|
+
import { NOTIFICATIONS_RETRIEVAL_ACTION } from '../constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves Android notifications via Appium Settings helper.
|
|
6
|
+
* Appium Settings app itself must be *manually* granted to access notifications
|
|
7
|
+
* under device Settings in order to make this feature working.
|
|
8
|
+
* Appium Settings helper keeps all the active notifications plus
|
|
9
|
+
* notifications that appeared while it was running in the internal buffer,
|
|
10
|
+
* but no more than 100 items altogether. Newly appeared notifications
|
|
11
|
+
* are always added to the head of the notifications array.
|
|
12
|
+
* The `isRemoved` flag is set to `true` for notifications that have been removed.
|
|
13
|
+
*
|
|
14
|
+
* See https://developer.android.com/reference/android/service/notification/StatusBarNotification
|
|
15
|
+
* and https://developer.android.com/reference/android/app/Notification.html
|
|
16
|
+
* for more information on available notification properties and their values.
|
|
17
|
+
*
|
|
18
|
+
* @this {import('../client').SettingsApp}
|
|
19
|
+
* @returns {Promise<Record<string, any>>} The example output is:
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "statusBarNotifications":[
|
|
23
|
+
* {
|
|
24
|
+
* "isGroup":false,
|
|
25
|
+
* "packageName":"io.appium.settings",
|
|
26
|
+
* "isClearable":false,
|
|
27
|
+
* "isOngoing":true,
|
|
28
|
+
* "id":1,
|
|
29
|
+
* "tag":null,
|
|
30
|
+
* "notification":{
|
|
31
|
+
* "title":null,
|
|
32
|
+
* "bigTitle":"Appium Settings",
|
|
33
|
+
* "text":null,
|
|
34
|
+
* "bigText":"Keep this service running, so Appium for Android can properly interact with several system APIs",
|
|
35
|
+
* "tickerText":null,
|
|
36
|
+
* "subText":null,
|
|
37
|
+
* "infoText":null,
|
|
38
|
+
* "template":"android.app.Notification$BigTextStyle"
|
|
39
|
+
* },
|
|
40
|
+
* "userHandle":0,
|
|
41
|
+
* "groupKey":"0|io.appium.settings|1|null|10133",
|
|
42
|
+
* "overrideGroupKey":null,
|
|
43
|
+
* "postTime":1576853518850,
|
|
44
|
+
* "key":"0|io.appium.settings|1|null|10133",
|
|
45
|
+
* "isRemoved":false
|
|
46
|
+
* }
|
|
47
|
+
* ]
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
* @throws {Error} If there was an error while getting the notifications list
|
|
51
|
+
*/
|
|
52
|
+
export async function getNotifications () {
|
|
53
|
+
this.log.debug(LOG_PREFIX, 'Retrieving notifications');
|
|
54
|
+
// Somehow providing the `-n` arg to the `am` underneath
|
|
55
|
+
// renders the broadcast to fail instead of starting the
|
|
56
|
+
// Appium Settings app. This only happens to the notifications
|
|
57
|
+
// receiver
|
|
58
|
+
await this.requireRunning({shouldRestoreCurrentApp: true});
|
|
59
|
+
let output;
|
|
60
|
+
try {
|
|
61
|
+
output = await this.adb.shell([
|
|
62
|
+
'am', 'broadcast',
|
|
63
|
+
'-a', NOTIFICATIONS_RETRIEVAL_ACTION,
|
|
64
|
+
]);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(`Cannot retrieve notifications from the device. ` +
|
|
67
|
+
`Make sure the Appium Settings application is installed and is up to date. ` +
|
|
68
|
+
`Original error: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
return this._parseJsonData(output, 'notifications');
|
|
71
|
+
};
|