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.
Files changed (69) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/apks/settings_apk-debug.apk +0 -0
  3. package/build/lib/client.d.ts +83 -0
  4. package/build/lib/client.d.ts.map +1 -0
  5. package/build/lib/client.js +137 -0
  6. package/build/lib/client.js.map +1 -0
  7. package/build/lib/commands/animation.d.ts +16 -0
  8. package/build/lib/commands/animation.d.ts.map +1 -0
  9. package/build/lib/commands/animation.js +29 -0
  10. package/build/lib/commands/animation.js.map +1 -0
  11. package/build/lib/commands/clipboard.d.ts +16 -0
  12. package/build/lib/commands/clipboard.d.ts.map +1 -0
  13. package/build/lib/commands/clipboard.js +51 -0
  14. package/build/lib/commands/clipboard.js.map +1 -0
  15. package/build/lib/commands/geolocation.d.ts +70 -0
  16. package/build/lib/commands/geolocation.d.ts.map +1 -0
  17. package/build/lib/commands/geolocation.js +215 -0
  18. package/build/lib/commands/geolocation.js.map +1 -0
  19. package/build/lib/commands/locale.d.ts +13 -0
  20. package/build/lib/commands/locale.d.ts.map +1 -0
  21. package/build/lib/commands/locale.js +31 -0
  22. package/build/lib/commands/locale.js.map +1 -0
  23. package/build/lib/commands/media.d.ts +11 -0
  24. package/build/lib/commands/media.d.ts.map +1 -0
  25. package/build/lib/commands/media.js +35 -0
  26. package/build/lib/commands/media.js.map +1 -0
  27. package/build/lib/commands/network.d.ts +19 -0
  28. package/build/lib/commands/network.d.ts.map +1 -0
  29. package/build/lib/commands/network.js +68 -0
  30. package/build/lib/commands/network.js.map +1 -0
  31. package/build/lib/commands/notifications.d.ts +50 -0
  32. package/build/lib/commands/notifications.d.ts.map +1 -0
  33. package/build/lib/commands/notifications.js +77 -0
  34. package/build/lib/commands/notifications.js.map +1 -0
  35. package/build/lib/commands/sms.d.ts +90 -0
  36. package/build/lib/commands/sms.d.ts.map +1 -0
  37. package/build/lib/commands/sms.js +95 -0
  38. package/build/lib/commands/sms.js.map +1 -0
  39. package/build/lib/commands/typing.d.ts +23 -0
  40. package/build/lib/commands/typing.d.ts.map +1 -0
  41. package/build/lib/commands/typing.js +51 -0
  42. package/build/lib/commands/typing.js.map +1 -0
  43. package/build/lib/commands/utf7.d.ts +26 -0
  44. package/build/lib/commands/utf7.d.ts.map +1 -0
  45. package/build/lib/commands/utf7.js +144 -0
  46. package/build/lib/commands/utf7.js.map +1 -0
  47. package/build/lib/constants.d.ts +23 -0
  48. package/build/lib/constants.d.ts.map +1 -0
  49. package/build/lib/constants.js +26 -0
  50. package/build/lib/constants.js.map +1 -0
  51. package/build/lib/logger.d.ts +3 -0
  52. package/build/lib/logger.d.ts.map +1 -0
  53. package/build/lib/logger.js +17 -0
  54. package/build/lib/logger.js.map +1 -0
  55. package/index.js +6 -2
  56. package/lib/client.js +155 -0
  57. package/lib/commands/animation.js +24 -0
  58. package/lib/commands/clipboard.js +47 -0
  59. package/lib/commands/geolocation.js +214 -0
  60. package/lib/commands/locale.js +28 -0
  61. package/lib/commands/media.js +27 -0
  62. package/lib/commands/network.js +73 -0
  63. package/lib/commands/notifications.js +71 -0
  64. package/lib/commands/sms.js +92 -0
  65. package/lib/commands/typing.js +46 -0
  66. package/lib/commands/utf7.js +154 -0
  67. package/lib/constants.js +31 -0
  68. package/lib/logger.js +14 -0
  69. 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
+ };