appium-adb 14.0.4 → 14.0.6

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.
@@ -3,7 +3,7 @@ import {
3
3
  APK_INSTALL_TIMEOUT, DEFAULT_ADB_EXEC_TIMEOUT,
4
4
  readPackageManifest
5
5
  } from '../helpers.js';
6
- import { exec } from 'teen_process';
6
+ import { exec, type ExecError } from 'teen_process';
7
7
  import { log } from '../logger.js';
8
8
  import path from 'path';
9
9
  import _ from 'lodash';
@@ -11,19 +11,31 @@ import { fs, util, mkdirp, timing } from '@appium/support';
11
11
  import * as semver from 'semver';
12
12
  import os from 'os';
13
13
  import { LRUCache } from 'lru-cache';
14
+ import type { ADB } from '../adb.js';
15
+ import type {
16
+ UninstallOptions,
17
+ ShellExecOptions,
18
+ CachingOptions,
19
+ InstallOptions,
20
+ InstallOrUpgradeOptions,
21
+ InstallOrUpgradeResult,
22
+ ApkStrings,
23
+ AppInfo,
24
+ InstallState,
25
+ StringRecord
26
+ } from './types.js';
14
27
 
15
28
  export const REMOTE_CACHE_ROOT = '/data/local/tmp/appium_cache';
16
29
 
17
30
  /**
18
31
  * Uninstall the given package from the device under test.
19
32
  *
20
- * @this {import('../adb.js').ADB}
21
- * @param {string} pkg - The name of the package to be uninstalled.
22
- * @param {import('./types').UninstallOptions} [options={}] - The set of uninstall options.
23
- * @return {Promise<boolean>} True if the package was found on the device and
33
+ * @param pkg - The name of the package to be uninstalled.
34
+ * @param options - The set of uninstall options.
35
+ * @returns True if the package was found on the device and
24
36
  * successfully uninstalled.
25
37
  */
26
- export async function uninstallApk (pkg, options = {}) {
38
+ export async function uninstallApk (this: ADB, pkg: string, options: UninstallOptions = {}): Promise<boolean> {
27
39
  log.debug(`Uninstalling ${pkg}`);
28
40
  if (!options.skipInstallCheck && !await this.isAppInstalled(pkg)) {
29
41
  log.info(`${pkg} was not uninstalled, because it was not present on the device`);
@@ -36,12 +48,13 @@ export async function uninstallApk (pkg, options = {}) {
36
48
  }
37
49
  cmd.push(pkg);
38
50
 
39
- let stdout;
51
+ let stdout: string;
40
52
  try {
41
53
  await this.forceStop(pkg);
42
54
  stdout = (await this.adbExec(cmd, {timeout: options.timeout})).trim();
43
55
  } catch (e) {
44
- throw new Error(`Unable to uninstall APK. Original error: ${e.message}`);
56
+ const err = e as Error;
57
+ throw new Error(`Unable to uninstall APK. Original error: ${err.message}`);
45
58
  }
46
59
  log.debug(`'adb ${cmd.join(' ')}' command output: ${stdout}`);
47
60
  if (stdout.includes('Success')) {
@@ -55,13 +68,12 @@ export async function uninstallApk (pkg, options = {}) {
55
68
  /**
56
69
  * Install the package after it was pushed to the device under test.
57
70
  *
58
- * @this {import('../adb.js').ADB}
59
- * @param {string} apkPathOnDevice - The full path to the package on the device file system.
60
- * @param {import('./types').ShellExecOptions} [opts={}] Additional exec options.
61
- * @throws {error} If there was a failure during application install.
71
+ * @param apkPathOnDevice - The full path to the package on the device file system.
72
+ * @param opts - Additional exec options.
73
+ * @throws If there was a failure during application install.
62
74
  */
63
- export async function installFromDevicePath (apkPathOnDevice, opts = {}) {
64
- const stdout = /** @type {string} */ (await this.shell(['pm', 'install', '-r', apkPathOnDevice], opts));
75
+ export async function installFromDevicePath (this: ADB, apkPathOnDevice: string, opts: ShellExecOptions = {}): Promise<void> {
76
+ const stdout = await this.shell(['pm', 'install', '-r', apkPathOnDevice], opts);
65
77
  if (stdout.includes('Failure')) {
66
78
  throw new Error(`Remote install failed: ${stdout}`);
67
79
  }
@@ -70,20 +82,19 @@ export async function installFromDevicePath (apkPathOnDevice, opts = {}) {
70
82
  /**
71
83
  * Caches the given APK at a remote location to speed up further APK deployments.
72
84
  *
73
- * @this {import('../adb.js').ADB}
74
- * @param {string} apkPath - Full path to the apk on the local FS
75
- * @param {import('./types').CachingOptions} [options={}] - Caching options
76
- * @returns {Promise<string>} - Full path to the cached apk on the remote file system
77
- * @throws {Error} if there was a failure while caching the app
85
+ * @param apkPath - Full path to the apk on the local FS
86
+ * @param options - Caching options
87
+ * @returns Full path to the cached apk on the remote file system
88
+ * @throws if there was a failure while caching the app
78
89
  */
79
- export async function cacheApk (apkPath, options = {}) {
90
+ export async function cacheApk (this: ADB, apkPath: string, options: CachingOptions = {}): Promise<string> {
80
91
  const appHash = await fs.hash(apkPath);
81
92
  const remotePath = path.posix.join(REMOTE_CACHE_ROOT, `${appHash}.apk`);
82
- const remoteCachedFiles = [];
93
+ const remoteCachedFiles: string[] = [];
83
94
  // Get current contents of the remote cache or create it for the first time
84
95
  try {
85
96
  const errorMarker = '_ERROR_';
86
- let lsOutput = null;
97
+ let lsOutput: string | null = null;
87
98
  if (this._areExtendedLsOptionsSupported === true || !_.isBoolean(this._areExtendedLsOptionsSupported)) {
88
99
  lsOutput = await this.shell([`ls -t -1 ${REMOTE_CACHE_ROOT} 2>&1 || echo ${errorMarker}`]);
89
100
  }
@@ -106,12 +117,13 @@ export async function cacheApk (apkPath, options = {}) {
106
117
  .filter(Boolean)
107
118
  ));
108
119
  } catch (e) {
109
- log.debug(`Got an error '${e.message.trim()}' while getting the list of files in the cache. ` +
120
+ const err = e as Error;
121
+ log.debug(`Got an error '${err.message.trim()}' while getting the list of files in the cache. ` +
110
122
  `Assuming the cache does not exist yet`);
111
123
  await this.shell(['mkdir', '-p', REMOTE_CACHE_ROOT]);
112
124
  }
113
125
  log.debug(`The count of applications in the cache: ${remoteCachedFiles.length}`);
114
- const toHash = (remotePath) => path.posix.parse(remotePath).name;
126
+ const toHash = (remotePath: string) => path.posix.parse(remotePath).name;
115
127
  // Push the apk to the remote cache if needed
116
128
  if (remoteCachedFiles.some((x) => toHash(x) === appHash)) {
117
129
  log.info(`The application at '${apkPath}' is already cached to '${remotePath}'`);
@@ -129,26 +141,27 @@ export async function cacheApk (apkPath, options = {}) {
129
141
  }
130
142
  if (!this.remoteAppsCache) {
131
143
  this.remoteAppsCache = new LRUCache({
132
- max: /** @type {number} */ (this.remoteAppsCacheLimit),
144
+ max: this.remoteAppsCacheLimit as number,
133
145
  });
134
146
  }
135
147
  // Cleanup the invalid entries from the cache
136
148
  _.difference([...this.remoteAppsCache.keys()], remoteCachedFiles.map(toHash))
137
- .forEach((hash) => (/** @type {LRUCache} */ (this.remoteAppsCache)).delete(hash));
149
+ .forEach((hash) => (this.remoteAppsCache as LRUCache<string, string>).delete(hash));
138
150
  // Bump the cache record for the recently cached item
139
151
  this.remoteAppsCache.set(appHash, remotePath);
140
152
  // If the remote cache exceeds this.remoteAppsCacheLimit, remove the least recently used entries
141
153
  const entriesToCleanup = remoteCachedFiles
142
154
  .map((x) => path.posix.join(REMOTE_CACHE_ROOT, x))
143
- .filter((x) => !(/** @type {LRUCache} */ (this.remoteAppsCache)).has(toHash(x)))
144
- .slice((/** @type {number} */ (this.remoteAppsCacheLimit)) - [...this.remoteAppsCache.keys()].length);
155
+ .filter((x) => !(this.remoteAppsCache as LRUCache<string, string>).has(toHash(x)))
156
+ .slice((this.remoteAppsCacheLimit as number) - [...this.remoteAppsCache.keys()].length);
145
157
  if (!_.isEmpty(entriesToCleanup)) {
146
158
  try {
147
159
  await this.shell(['rm', '-f', ...entriesToCleanup]);
148
160
  log.debug(`Deleted ${entriesToCleanup.length} expired application cache entries`);
149
161
  } catch (e) {
162
+ const err = e as Error;
150
163
  log.warn(`Cannot delete ${entriesToCleanup.length} expired application cache entries. ` +
151
- `Original error: ${e.message}`);
164
+ `Original error: ${err.message}`);
152
165
  }
153
166
  }
154
167
  return remotePath;
@@ -157,12 +170,11 @@ export async function cacheApk (apkPath, options = {}) {
157
170
  /**
158
171
  * Install the package from the local file system.
159
172
  *
160
- * @this {import('../adb.js').ADB}
161
- * @param {string} appPath - The full path to the local package.
162
- * @param {import('./types').InstallOptions} [options={}] - The set of installation options.
163
- * @throws {Error} If an unexpected error happens during install.
173
+ * @param appPath - The full path to the local package.
174
+ * @param options - The set of installation options.
175
+ * @throws If an unexpected error happens during install.
164
176
  */
165
- export async function install (appPath, options = {}) {
177
+ export async function install (this: ADB, appPath: string, options: InstallOptions = {}): Promise<void> {
166
178
  if (appPath.endsWith(APKS_EXTENSION)) {
167
179
  return await this.installApks(appPath, options);
168
180
  }
@@ -191,7 +203,7 @@ export async function install (appPath, options = {}) {
191
203
  ];
192
204
  let performAppInstall = async () => await this.adbExec(installCmd, installOpts);
193
205
  // this.remoteAppsCacheLimit <= 0 means no caching should be applied
194
- let shouldCacheApp = (/** @type {number} */ (this.remoteAppsCacheLimit)) > 0;
206
+ let shouldCacheApp = (this.remoteAppsCacheLimit as number) > 0;
195
207
  if (shouldCacheApp) {
196
208
  shouldCacheApp = !(await this.isStreamedInstallSupported());
197
209
  if (!shouldCacheApp) {
@@ -210,7 +222,7 @@ export async function install (appPath, options = {}) {
210
222
  try {
211
223
  const cachedAppPath = await cacheApp();
212
224
  performAppInstall = async () => {
213
- const pmInstallCmdByRemotePath = (remotePath) => [
225
+ const pmInstallCmdByRemotePath = (remotePath: string) => [
214
226
  'pm', 'install',
215
227
  ...installArgs,
216
228
  remotePath,
@@ -229,15 +241,16 @@ export async function install (appPath, options = {}) {
229
241
  return output;
230
242
  };
231
243
  } catch (e) {
232
- log.debug(e);
233
- log.warn(`There was a failure while caching '${appPath}': ${e.message}`);
244
+ const err = e as Error;
245
+ log.debug(err);
246
+ log.warn(`There was a failure while caching '${appPath}': ${err.message}`);
234
247
  log.warn('Falling back to the default installation procedure');
235
248
  await clearCache();
236
249
  }
237
250
  }
238
251
  try {
239
252
  const timer = new timing.Timer().start();
240
- const output = /** @type {string} */(await performAppInstall());
253
+ const output = await performAppInstall();
241
254
  log.info(`The installation of '${path.basename(appPath)}' took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
242
255
  const truncatedOutput = (!_.isString(output) || output.length <= 300) ?
243
256
  output : `${output.substring(0, 150)}...${output.substring(output.length - 150)}`;
@@ -251,10 +264,11 @@ export async function install (appPath, options = {}) {
251
264
  throw new Error(output);
252
265
  }
253
266
  } catch (err) {
267
+ const error = err as Error;
254
268
  // on some systems this will throw an error if the app already
255
269
  // exists
256
- if (!err.message.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
257
- throw err;
270
+ if (!error.message.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
271
+ throw error;
258
272
  }
259
273
  log.debug(`Application '${appPath}' already installed. Continuing.`);
260
274
  }
@@ -263,18 +277,16 @@ export async function install (appPath, options = {}) {
263
277
  /**
264
278
  * Retrieves the current installation state of the particular application
265
279
  *
266
- * @this {import('../adb.js').ADB}
267
- * @param {string} appPath - Full path to the application
268
- * @param {string?} [pkg=null] - Package identifier. If omitted then the script will
280
+ * @param appPath - Full path to the application
281
+ * @param pkg - Package identifier. If omitted then the script will
269
282
  * try to extract it on its own
270
- * @returns {Promise<import('./types').InstallState>} One of `APP_INSTALL_STATE` constants
283
+ * @returns One of `APP_INSTALL_STATE` constants
271
284
  */
272
- export async function getApplicationInstallState (appPath, pkg = null) {
273
- let apkInfo = null;
285
+ export async function getApplicationInstallState (this: ADB, appPath: string, pkg: string | null = null): Promise<InstallState> {
286
+ let apkInfo: AppInfo | null = null;
274
287
  if (!pkg) {
275
- apkInfo = await this.getApkInfo(appPath);
276
- // @ts-ignore We are ok if this prop does not exist
277
- pkg = apkInfo.name;
288
+ apkInfo = (await this.getApkInfo(appPath)) as AppInfo;
289
+ pkg = apkInfo?.name;
278
290
  }
279
291
  if (!pkg) {
280
292
  log.warn(`Cannot read the package name of '${appPath}'`);
@@ -292,10 +304,10 @@ export async function getApplicationInstallState (appPath, pkg = null) {
292
304
  }
293
305
  const pkgVersionName = semver.valid(semver.coerce(pkgVersionNameStr));
294
306
  if (!apkInfo) {
295
- apkInfo = await this.getApkInfo(appPath);
307
+ apkInfo = (await this.getApkInfo(appPath)) as AppInfo;
296
308
  }
297
309
  // @ts-ignore We validate the values below
298
- const {versionCode: apkVersionCode, versionName: apkVersionNameStr} = apkInfo;
310
+ const {versionCode: apkVersionCode, versionName: apkVersionNameStr} = apkInfo || {};
299
311
  const apkVersionName = semver.valid(semver.coerce(apkVersionNameStr));
300
312
 
301
313
  if (!_.isInteger(apkVersionCode) || !_.isInteger(pkgVersionCode)) {
@@ -306,7 +318,7 @@ export async function getApplicationInstallState (appPath, pkg = null) {
306
318
  }
307
319
  }
308
320
  if (_.isInteger(apkVersionCode) && _.isInteger(pkgVersionCode)) {
309
- if ((/** @type {number} */ (pkgVersionCode)) > apkVersionCode) {
321
+ if ((pkgVersionCode as number) > (apkVersionCode as number)) {
310
322
  log.debug(`The version code of the installed '${pkg}' is greater than the application version code (${pkgVersionCode} > ${apkVersionCode})`);
311
323
  return this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED;
312
324
  }
@@ -338,15 +350,13 @@ export async function getApplicationInstallState (appPath, pkg = null) {
338
350
  * Install the package from the local file system or upgrade it if an older
339
351
  * version of the same package is already installed.
340
352
  *
341
- * @this {import('../adb.js').ADB}
342
- * @param {string} appPath - The full path to the local package.
343
- * @param {string?} [pkg=null] - The name of the installed package. The method will
353
+ * @param appPath - The full path to the local package.
354
+ * @param pkg - The name of the installed package. The method will
344
355
  * perform faster if it is set.
345
- * @param {import('./types').InstallOrUpgradeOptions} [options={}] - Set of install options.
346
- * @throws {Error} If an unexpected error happens during install.
347
- * @returns {Promise<import('./types').InstallOrUpgradeResult>}
356
+ * @param options - Set of install options.
357
+ * @throws If an unexpected error happens during install.
348
358
  */
349
- export async function installOrUpgrade (appPath, pkg = null, options = {}) {
359
+ export async function installOrUpgrade (this: ADB, appPath: string, pkg: string | null = null, options: InstallOrUpgradeOptions = {}): Promise<InstallOrUpgradeResult> {
350
360
  if (!pkg) {
351
361
  const apkInfo = await this.getApkInfo(appPath);
352
362
  if ('name' in apkInfo) {
@@ -365,7 +375,7 @@ export async function installOrUpgrade (appPath, pkg = null, options = {}) {
365
375
  const appState = await this.getApplicationInstallState(appPath, pkg);
366
376
  let wasUninstalled = false;
367
377
  const uninstallPackage = async () => {
368
- if (!await this.uninstallApk(/** @type {string} */ (pkg), {skipInstallCheck: true})) {
378
+ if (!await this.uninstallApk(pkg as string, {skipInstallCheck: true})) {
369
379
  throw new Error(`'${pkg}' package cannot be uninstalled`);
370
380
  }
371
381
  wasUninstalled = true;
@@ -409,7 +419,8 @@ export async function installOrUpgrade (appPath, pkg = null, options = {}) {
409
419
  try {
410
420
  await this.install(appPath, {...options, replace: true});
411
421
  } catch (err) {
412
- log.warn(`Cannot install/upgrade '${pkg}' because of '${err.message}'. Trying full reinstall`);
422
+ const error = err as Error;
423
+ log.warn(`Cannot install/upgrade '${pkg}' because of '${error.message}'. Trying full reinstall`);
413
424
  await uninstallPackage();
414
425
  await this.install(appPath, {...options, replace: false});
415
426
  }
@@ -422,63 +433,64 @@ export async function installOrUpgrade (appPath, pkg = null, options = {}) {
422
433
  /**
423
434
  * Extract string resources from the given package on local file system.
424
435
  *
425
- * @this {import('../adb.js').ADB}
426
- * @param {string} appPath - The full path to the .apk(s) package.
427
- * @param {string?} [language=null] - The name of the language to extract the resources for.
436
+ * @param appPath - The full path to the .apk(s) package.
437
+ * @param language - The name of the language to extract the resources for.
428
438
  * The default language is used if this equals to `null`
429
- * @param {string?} [outRoot=null] - The name of the destination folder on the local file system to
439
+ * @param outRoot - The name of the destination folder on the local file system to
430
440
  * store the extracted file to. If not provided then the `localPath` property in the returned object
431
441
  * will be undefined.
432
- * @return {Promise<import('./types').ApkStrings>}
433
442
  */
434
443
  export async function extractStringsFromApk (
435
- appPath,
436
- language = null,
437
- outRoot = null
438
- ) {
444
+ this: ADB,
445
+ appPath: string,
446
+ language: string | null = null,
447
+ outRoot: string | null = null
448
+ ): Promise<ApkStrings> {
439
449
  log.debug(`Extracting strings from for language: ${language || 'default'}`);
440
450
  const originalAppPath = appPath;
441
451
  if (appPath.endsWith(APKS_EXTENSION)) {
442
452
  appPath = await this.extractLanguageApk(appPath, language);
443
453
  }
444
454
 
445
- let apkStrings = {};
446
- let configMarker;
455
+ let apkStrings: StringRecord = {};
456
+ let configMarker: string;
447
457
  try {
448
458
  await this.initAapt();
449
459
 
450
460
  configMarker = await formatConfigMarker(async () => {
451
- const {stdout} = await exec((/** @type {import('./types').StringRecord} */ (this.binaries)).aapt, [
461
+ const {stdout} = await exec((this.binaries as StringRecord).aapt as string, [
452
462
  'd', 'configurations', appPath,
453
463
  ]);
454
464
  return _.uniq(stdout.split(os.EOL));
455
465
  }, language, '(default)');
456
466
 
457
- const {stdout} = await exec((/** @type {import('./types').StringRecord} */ (this.binaries)).aapt, [
467
+ const {stdout} = await exec((this.binaries as StringRecord).aapt as string, [
458
468
  'd', '--values', 'resources', appPath,
459
469
  ]);
460
470
  apkStrings = parseAaptStrings(stdout, configMarker);
461
471
  } catch (e) {
472
+ const err = e as ExecError;
462
473
  log.debug('Cannot extract resources using aapt. Trying aapt2. ' +
463
- `Original error: ${e.stderr || e.message}`);
474
+ `Original error: ${err.stderr || err.message}`);
464
475
 
465
476
  await this.initAapt2();
466
477
 
467
478
  configMarker = await formatConfigMarker(async () => {
468
- const {stdout} = await exec((/** @type {import('./types').StringRecord} */ (this.binaries)).aapt2, [
479
+ const {stdout} = await exec((this.binaries as StringRecord).aapt2 as string, [
469
480
  'd', 'configurations', appPath,
470
481
  ]);
471
482
  return _.uniq(stdout.split(os.EOL));
472
483
  }, language, '');
473
484
 
474
485
  try {
475
- const {stdout} = await exec((/** @type {import('./types').StringRecord} */ (this.binaries)).aapt2, [
486
+ const {stdout} = await exec((this.binaries as StringRecord).aapt2 as string, [
476
487
  'd', 'resources', appPath,
477
488
  ]);
478
489
  apkStrings = parseAapt2Strings(stdout, configMarker);
479
490
  } catch (e) {
491
+ const error = e as Error;
480
492
  throw new Error(`Cannot extract resources from '${originalAppPath}'. ` +
481
- `Original error: ${e.message}`);
493
+ `Original error: ${error.message}`);
482
494
  }
483
495
  }
484
496
 
@@ -503,12 +515,11 @@ export async function extractStringsFromApk (
503
515
  /**
504
516
  * Get the package info from local apk file.
505
517
  *
506
- * @this {import('../adb.js').ADB}
507
- * @param {string} appPath - The full path to existing .apk(s) package on the local
518
+ * @param appPath - The full path to existing .apk(s) package on the local
508
519
  * file system.
509
- * @return {Promise<import('./types').AppInfo|{}>} The parsed application information.
520
+ * @returns The parsed application information.
510
521
  */
511
- export async function getApkInfo (appPath) {
522
+ export async function getApkInfo (this: ADB, appPath: string): Promise<AppInfo | {}> {
512
523
  if (!await fs.exists(appPath)) {
513
524
  throw new Error(`The file at path ${appPath} does not exist or is not accessible`);
514
525
  }
@@ -525,7 +536,8 @@ export async function getApkInfo (appPath) {
525
536
  versionName,
526
537
  };
527
538
  } catch (e) {
528
- log.warn(`Error '${e.message}' while getting badging info`);
539
+ const err = e as Error;
540
+ log.warn(`Error '${err.message}' while getting badging info`);
529
541
  }
530
542
  return {};
531
543
  }
@@ -536,13 +548,13 @@ export async function getApkInfo (appPath) {
536
548
  * Formats the config marker, which is then passed to parse.. methods
537
549
  * to make it compatible with resource formats generated by aapt(2) tool
538
550
  *
539
- * @param {Function} configsGetter The function whose result is a list
551
+ * @param configsGetter The function whose result is a list
540
552
  * of apk configs
541
- * @param {string?} desiredMarker The desired config marker value
542
- * @param {string} defaultMarker The default config marker value
543
- * @return {Promise<string>} The formatted config marker
553
+ * @param desiredMarker The desired config marker value
554
+ * @param defaultMarker The default config marker value
555
+ * @returns The formatted config marker
544
556
  */
545
- async function formatConfigMarker (configsGetter, desiredMarker, defaultMarker) {
557
+ async function formatConfigMarker (configsGetter: () => Promise<string[]>, desiredMarker: string | null, defaultMarker: string): Promise<string> {
546
558
  let configMarker = desiredMarker || defaultMarker;
547
559
  if (configMarker.includes('-') && !configMarker.includes('-r')) {
548
560
  configMarker = configMarker.replace('-', '-r');
@@ -564,16 +576,16 @@ async function formatConfigMarker (configsGetter, desiredMarker, defaultMarker)
564
576
  /**
565
577
  * Parses apk strings from aapt2 tool output
566
578
  *
567
- * @param {string} rawOutput The actual tool output
568
- * @param {string} configMarker The config marker. Usually
579
+ * @param rawOutput The actual tool output
580
+ * @param configMarker The config marker. Usually
569
581
  * a language abbreviation or an empty string for the default one
570
- * @returns {Object} Strings ids to values mapping. Plural
582
+ * @returns Strings ids to values mapping. Plural
571
583
  * values are represented as arrays. If no config found for the
572
584
  * given marker then an empty mapping is returned.
573
585
  */
574
- export function parseAapt2Strings (rawOutput, configMarker) {
586
+ export function parseAapt2Strings (rawOutput: string, configMarker: string): StringRecord {
575
587
  const allLines = rawOutput.split(os.EOL);
576
- function extractContent (startIdx) {
588
+ function extractContent (startIdx: number): [string | null, number] {
577
589
  let idx = startIdx;
578
590
  const startCharPos = allLines[startIdx].indexOf('"');
579
591
  if (startCharPos < 0) {
@@ -605,8 +617,8 @@ export function parseAapt2Strings (rawOutput, configMarker) {
605
617
  return [result, idx];
606
618
  }
607
619
 
608
- const apkStrings = {};
609
- let currentResourceId = null;
620
+ const apkStrings: StringRecord = {};
621
+ let currentResourceId: string | null = null;
610
622
  let isInPluralGroup = false;
611
623
  let isInCurrentConfig = false;
612
624
  let lineIndex = 0;
@@ -658,7 +670,7 @@ export function parseAapt2Strings (rawOutput, configMarker) {
658
670
  lineIndex = idx;
659
671
  if (_.isString(content)) {
660
672
  apkStrings[currentResourceId] = [
661
- ...(apkStrings[currentResourceId] || []),
673
+ ...(Array.isArray(apkStrings[currentResourceId]) ? apkStrings[currentResourceId] : []),
662
674
  content,
663
675
  ];
664
676
  }
@@ -680,21 +692,21 @@ export function parseAapt2Strings (rawOutput, configMarker) {
680
692
  /**
681
693
  * Parses apk strings from aapt tool output
682
694
  *
683
- * @param {string} rawOutput The actual tool output
684
- * @param {string} configMarker The config marker. Usually
695
+ * @param rawOutput The actual tool output
696
+ * @param configMarker The config marker. Usually
685
697
  * a language abbreviation or `(default)`
686
- * @returns {Object} Strings ids to values mapping. Plural
698
+ * @returns Strings ids to values mapping. Plural
687
699
  * values are represented as arrays. If no config found for the
688
700
  * given marker then an empty mapping is returned.
689
701
  */
690
- export function parseAaptStrings (rawOutput, configMarker) {
691
- const normalizeStringMatch = function (s) {
702
+ export function parseAaptStrings (rawOutput: string, configMarker: string): StringRecord {
703
+ const normalizeStringMatch = function (s: string) {
692
704
  return s.replace(/"$/, '').replace(/^"/, '').replace(/\\"/g, '"');
693
705
  };
694
706
 
695
- const apkStrings = {};
707
+ const apkStrings: StringRecord = {};
696
708
  let isInConfig = false;
697
- let currentResourceId = null;
709
+ let currentResourceId: string | null = null;
698
710
  let isInPluralGroup = false;
699
711
  // The pattern matches any quoted content including escaped quotes
700
712
  const quotedStringPattern = /"[^"\\]*(?:\\.[^"\\]*)*"/;
@@ -747,7 +759,7 @@ export function parseAaptStrings (rawOutput, configMarker) {
747
759
  const match = quotedStringPattern.exec(trimmedLine);
748
760
  if (match) {
749
761
  apkStrings[currentResourceId] = [
750
- ...(apkStrings[currentResourceId] || []),
762
+ ...(Array.isArray(apkStrings[currentResourceId]) ? apkStrings[currentResourceId] : []),
751
763
  normalizeStringMatch(match[0]),
752
764
  ];
753
765
  }
@@ -758,3 +770,4 @@ export function parseAaptStrings (rawOutput, configMarker) {
758
770
  }
759
771
 
760
772
  // #endregion
773
+