UNPKG

appium-adb

Version:

Android Debug Bridge interface

722 lines 30.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.REMOTE_CACHE_ROOT = void 0; exports.uninstallApk = uninstallApk; exports.installFromDevicePath = installFromDevicePath; exports.cacheApk = cacheApk; exports.install = install; exports.getApplicationInstallState = getApplicationInstallState; exports.installOrUpgrade = installOrUpgrade; exports.extractStringsFromApk = extractStringsFromApk; exports.getApkInfo = getApkInfo; exports.parseAapt2Strings = parseAapt2Strings; exports.parseAaptStrings = parseAaptStrings; const helpers_1 = require("../helpers"); const teen_process_1 = require("teen_process"); const logger_1 = require("../logger"); const node_path_1 = __importDefault(require("node:path")); const lodash_1 = __importDefault(require("lodash")); const support_1 = require("@appium/support"); const semver = __importStar(require("semver")); const node_os_1 = __importDefault(require("node:os")); const lru_cache_1 = require("lru-cache"); exports.REMOTE_CACHE_ROOT = '/data/local/tmp/appium_cache'; /** * Uninstall the given package from the device under test. * * @param pkg - The name of the package to be uninstalled. * @param options - The set of uninstall options. * @returns True if the package was found on the device and * successfully uninstalled. */ async function uninstallApk(pkg, options = {}) { logger_1.log.debug(`Uninstalling ${pkg}`); if (!options.skipInstallCheck && !(await this.isAppInstalled(pkg))) { logger_1.log.info(`${pkg} was not uninstalled, because it was not present on the device`); return false; } const cmd = ['uninstall']; if (options.keepData) { cmd.push('-k'); } cmd.push(pkg); let stdout; try { await this.forceStop(pkg); stdout = (await this.adbExec(cmd, { timeout: options.timeout })).trim(); } catch (e) { const err = e; throw new Error(`Unable to uninstall APK. Original error: ${err.message}`); } logger_1.log.debug(`'adb ${cmd.join(' ')}' command output: ${stdout}`); if (stdout.includes('Success')) { logger_1.log.info(`${pkg} was successfully uninstalled`); return true; } logger_1.log.info(`${pkg} was not uninstalled`); return false; } /** * Install the package after it was pushed to the device under test. * * @param apkPathOnDevice - The full path to the package on the device file system. * @param opts - Additional exec options. * @throws If there was a failure during application install. */ async function installFromDevicePath(apkPathOnDevice, opts = {}) { const stdout = await this.shell(['pm', 'install', '-r', apkPathOnDevice], opts); if (stdout.includes('Failure')) { throw new Error(`Remote install failed: ${stdout}`); } } /** * Caches the given APK at a remote location to speed up further APK deployments. * * @param apkPath - Full path to the apk on the local FS * @param options - Caching options * @returns Full path to the cached apk on the remote file system * @throws if there was a failure while caching the app */ async function cacheApk(apkPath, options = {}) { const appHash = await support_1.fs.hash(apkPath); const remotePath = node_path_1.default.posix.join(exports.REMOTE_CACHE_ROOT, `${appHash}.apk`); const remoteCachedFiles = []; // Get current contents of the remote cache or create it for the first time try { const errorMarker = '_ERROR_'; let lsOutput = null; if (this._areExtendedLsOptionsSupported === true || !lodash_1.default.isBoolean(this._areExtendedLsOptionsSupported)) { lsOutput = await this.shell([`ls -t -1 ${exports.REMOTE_CACHE_ROOT} 2>&1 || echo ${errorMarker}`]); } if (!lodash_1.default.isString(lsOutput) || (lsOutput.includes(errorMarker) && !lsOutput.includes(exports.REMOTE_CACHE_ROOT))) { if (!lodash_1.default.isBoolean(this._areExtendedLsOptionsSupported)) { logger_1.log.debug('The current Android API does not support extended ls options. ' + 'Defaulting to no-options call'); } lsOutput = await this.shell([`ls ${exports.REMOTE_CACHE_ROOT} 2>&1 || echo ${errorMarker}`]); this._areExtendedLsOptionsSupported = false; } else { this._areExtendedLsOptionsSupported = true; } if (lsOutput.includes(errorMarker)) { throw new Error(lsOutput.substring(0, lsOutput.indexOf(errorMarker))); } remoteCachedFiles.push(...lsOutput .split('\n') .map((x) => x.trim()) .filter(Boolean)); } catch (e) { const err = e; logger_1.log.debug(`Got an error '${err.message.trim()}' while getting the list of files in the cache. ` + `Assuming the cache does not exist yet`); await this.shell(['mkdir', '-p', exports.REMOTE_CACHE_ROOT]); } logger_1.log.debug(`The count of applications in the cache: ${remoteCachedFiles.length}`); const toHash = (remotePath) => node_path_1.default.posix.parse(remotePath).name; // Push the apk to the remote cache if needed if (remoteCachedFiles.some((x) => toHash(x) === appHash)) { logger_1.log.info(`The application at '${apkPath}' is already cached to '${remotePath}'`); // Update the application timestamp asynchronously in order to bump its position // in the sorted ls output // eslint-disable-next-line promise/prefer-await-to-then this.shell(['touch', '-am', remotePath]).catch(() => { }); } else { logger_1.log.info(`Caching the application at '${apkPath}' to '${remotePath}'`); const timer = new support_1.timing.Timer().start(); await this.push(apkPath, remotePath, { timeout: options.timeout }); const { size } = await support_1.fs.stat(apkPath); logger_1.log.info(`The upload of '${node_path_1.default.basename(apkPath)}' (${support_1.util.toReadableSizeString(size)}) ` + `took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } if (!this.remoteAppsCache) { this.remoteAppsCache = new lru_cache_1.LRUCache({ max: this.remoteAppsCacheLimit, }); } // Cleanup the invalid entries from the cache lodash_1.default.difference([...this.remoteAppsCache.keys()], remoteCachedFiles.map(toHash)).forEach((hash) => this.remoteAppsCache.delete(hash)); // Bump the cache record for the recently cached item this.remoteAppsCache.set(appHash, remotePath); // If the remote cache exceeds this.remoteAppsCacheLimit, remove the least recently used entries const entriesToCleanup = remoteCachedFiles .map((x) => node_path_1.default.posix.join(exports.REMOTE_CACHE_ROOT, x)) .filter((x) => !this.remoteAppsCache.has(toHash(x))) .slice(this.remoteAppsCacheLimit - [...this.remoteAppsCache.keys()].length); if (!lodash_1.default.isEmpty(entriesToCleanup)) { try { await this.shell(['rm', '-f', ...entriesToCleanup]); logger_1.log.debug(`Deleted ${entriesToCleanup.length} expired application cache entries`); } catch (e) { const err = e; logger_1.log.warn(`Cannot delete ${entriesToCleanup.length} expired application cache entries. ` + `Original error: ${err.message}`); } } return remotePath; } /** * Install the package from the local file system. * * @param appPath - The full path to the local package. * @param options - The set of installation options. * @throws If an unexpected error happens during install. */ async function install(appPath, options = {}) { if (appPath.endsWith(helpers_1.APKS_EXTENSION)) { return await this.installApks(appPath, options); } options = lodash_1.default.cloneDeep(options); lodash_1.default.defaults(options, { replace: true, timeout: this.adbExecTimeout === helpers_1.DEFAULT_ADB_EXEC_TIMEOUT ? helpers_1.APK_INSTALL_TIMEOUT : this.adbExecTimeout, timeoutCapName: 'androidInstallTimeout', }); const installArgs = (0, helpers_1.buildInstallArgs)(await this.getApiLevel(), options); if (options.noIncremental && (await this.isIncrementalInstallSupported())) { // Adb throws an error if it does not know about an arg, // which is the case here for older adb versions. installArgs.push('--no-incremental'); } const installOpts = { timeout: options.timeout, timeoutCapName: options.timeoutCapName, }; const installCmd = ['install', ...installArgs, appPath]; try { const timer = new support_1.timing.Timer().start(); const output = await this.adbExec(installCmd, installOpts); logger_1.log.info(`The installation of '${node_path_1.default.basename(appPath)}' took ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); const truncatedOutput = !lodash_1.default.isString(output) || output.length <= 300 ? output : `${output.substring(0, 150)}...${output.substring(output.length - 150)}`; logger_1.log.debug(`Install command stdout: ${truncatedOutput}`); if (/\[INSTALL[A-Z_]+FAILED[A-Z_]+\]/.test(output)) { if (this.isTestPackageOnlyError(output)) { const msg = `Set 'allowTestPackages' capability to true in order to allow test packages installation.`; logger_1.log.warn(msg); throw new Error(`${output}\n${msg}`); } throw new Error(output); } } catch (err) { const error = err; // on some systems this will throw an error if the app already // exists if (!error.message.includes('INSTALL_FAILED_ALREADY_EXISTS')) { throw error; } logger_1.log.debug(`Application '${appPath}' already installed. Continuing.`); } } /** * Retrieves the current installation state of the particular application * * @param appPath - Full path to the application * @param pkg - Package identifier. If omitted then the script will * try to extract it on its own * @returns One of `APP_INSTALL_STATE` constants */ async function getApplicationInstallState(appPath, pkg = null) { let apkInfo = null; if (!pkg) { apkInfo = (await this.getApkInfo(appPath)); pkg = apkInfo?.name; } if (!pkg) { logger_1.log.warn(`Cannot read the package name of '${appPath}'`); return this.APP_INSTALL_STATE.UNKNOWN; } const { versionCode: pkgVersionCode, versionName: pkgVersionNameStr, isInstalled, } = await this.getPackageInfo(pkg); if (!isInstalled) { logger_1.log.debug(`App '${appPath}' is not installed`); return this.APP_INSTALL_STATE.NOT_INSTALLED; } const pkgVersionName = semver.valid(semver.coerce(pkgVersionNameStr)); if (!apkInfo) { apkInfo = (await this.getApkInfo(appPath)); } // @ts-ignore We validate the values below const { versionCode: apkVersionCode, versionName: apkVersionNameStr } = apkInfo || {}; const apkVersionName = semver.valid(semver.coerce(apkVersionNameStr)); if (!lodash_1.default.isInteger(apkVersionCode) || !lodash_1.default.isInteger(pkgVersionCode)) { logger_1.log.warn(`Cannot read version codes of '${appPath}' and/or '${pkg}'`); if (!lodash_1.default.isString(apkVersionName) || !lodash_1.default.isString(pkgVersionName)) { logger_1.log.warn(`Cannot read version names of '${appPath}' and/or '${pkg}'`); return this.APP_INSTALL_STATE.UNKNOWN; } } if (lodash_1.default.isInteger(apkVersionCode) && lodash_1.default.isInteger(pkgVersionCode)) { if (pkgVersionCode > apkVersionCode) { logger_1.log.debug(`The version code of the installed '${pkg}' is greater than the application version code (${pkgVersionCode} > ${apkVersionCode})`); return this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED; } // Version codes might not be maintained. Check version names. if (pkgVersionCode === apkVersionCode) { if (lodash_1.default.isString(apkVersionName) && lodash_1.default.isString(pkgVersionName) && semver.satisfies(pkgVersionName, `>=${apkVersionName}`)) { logger_1.log.debug(`The version name of the installed '${pkg}' is greater or equal to the application version name ('${pkgVersionName}' >= '${apkVersionName}')`); return semver.satisfies(pkgVersionName, `>${apkVersionName}`) ? this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED : this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } if (!lodash_1.default.isString(apkVersionName) || !lodash_1.default.isString(pkgVersionName)) { logger_1.log.debug(`The version name of the installed '${pkg}' is equal to application version name (${pkgVersionCode} === ${apkVersionCode})`); return this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } } } else if (lodash_1.default.isString(apkVersionName) && lodash_1.default.isString(pkgVersionName) && semver.satisfies(pkgVersionName, `>=${apkVersionName}`)) { logger_1.log.debug(`The version name of the installed '${pkg}' is greater or equal to the application version name ('${pkgVersionName}' >= '${apkVersionName}')`); return semver.satisfies(pkgVersionName, `>${apkVersionName}`) ? this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED : this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED; } logger_1.log.debug(`The installed '${pkg}' package is older than '${appPath}' (${pkgVersionCode} < ${apkVersionCode} or '${pkgVersionName}' < '${apkVersionName}')'`); return this.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED; } /** * Install the package from the local file system or upgrade it if an older * version of the same package is already installed. * * @param appPath - The full path to the local package. * @param pkg - The name of the installed package. The method will * perform faster if it is set. * @param options - Set of install options. * @throws If an unexpected error happens during install. */ async function installOrUpgrade(appPath, pkg = null, options = {}) { if (!pkg) { const apkInfo = await this.getApkInfo(appPath); if ('name' in apkInfo) { pkg = apkInfo.name; } else { logger_1.log.warn(`Cannot determine the package name of '${appPath}'. ` + `Continuing with the install anyway`); } } const { enforceCurrentBuild } = options; const appState = await this.getApplicationInstallState(appPath, pkg); let wasUninstalled = false; const uninstallPackage = async () => { if (!(await this.uninstallApk(pkg, { skipInstallCheck: true }))) { throw new Error(`'${pkg}' package cannot be uninstalled`); } wasUninstalled = true; }; switch (appState) { case this.APP_INSTALL_STATE.NOT_INSTALLED: logger_1.log.debug(`Installing '${appPath}'`); await this.install(appPath, { ...options, replace: false }); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.NEWER_VERSION_INSTALLED: if (enforceCurrentBuild) { logger_1.log.info(`Downgrading '${pkg}' as requested`); await uninstallPackage(); break; } logger_1.log.debug(`There is no need to downgrade '${pkg}'`); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.SAME_VERSION_INSTALLED: if (enforceCurrentBuild) { break; } logger_1.log.debug(`There is no need to install/upgrade '${appPath}'`); return { appState, wasUninstalled, }; case this.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED: logger_1.log.debug(`Executing upgrade of '${appPath}'`); break; default: logger_1.log.debug(`The current install state of '${appPath}' is unknown. Installing anyway`); break; } try { await this.install(appPath, { ...options, replace: true }); } catch (err) { const error = err; logger_1.log.warn(`Cannot install/upgrade '${pkg}' because of '${error.message}'. Trying full reinstall`); await uninstallPackage(); await this.install(appPath, { ...options, replace: false }); } return { appState, wasUninstalled, }; } /** * Extract string resources from the given package on local file system. * * @param appPath - The full path to the .apk(s) package. * @param language - The name of the language to extract the resources for. * The default language is used if this equals to `null` * @param outRoot - The name of the destination folder on the local file system to * store the extracted file to. If not provided then the `localPath` property in the returned object * will be undefined. */ async function extractStringsFromApk(appPath, language = null, outRoot = null) { logger_1.log.debug(`Extracting strings from for language: ${language || 'default'}`); const originalAppPath = appPath; if (appPath.endsWith(helpers_1.APKS_EXTENSION)) { appPath = await this.extractLanguageApk(appPath, language); } let apkStrings = {}; let configMarker; try { await this.initAapt(); configMarker = await formatConfigMarker(async () => { const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt, [ 'd', 'configurations', appPath, ]); return lodash_1.default.uniq(stdout.split(node_os_1.default.EOL)); }, language, '(default)'); const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt, [ 'd', '--values', 'resources', appPath, ]); apkStrings = parseAaptStrings(stdout, configMarker); } catch (e) { const err = e; logger_1.log.debug('Cannot extract resources using aapt. Trying aapt2. ' + `Original error: ${err.stderr || err.message}`); await this.initAapt2(); configMarker = await formatConfigMarker(async () => { const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt2, [ 'd', 'configurations', appPath, ]); return lodash_1.default.uniq(stdout.split(node_os_1.default.EOL)); }, language, ''); try { const { stdout } = await (0, teen_process_1.exec)(this.binaries.aapt2, [ 'd', 'resources', appPath, ]); apkStrings = parseAapt2Strings(stdout, configMarker); } catch (e) { const error = e; throw new Error(`Cannot extract resources from '${originalAppPath}'. ` + `Original error: ${error.message}`); } } if (lodash_1.default.isEmpty(apkStrings)) { logger_1.log.warn(`No strings have been found in '${originalAppPath}' resources ` + `for '${configMarker || 'default'}' configuration`); } else { logger_1.log.info(`Successfully extracted ${lodash_1.default.keys(apkStrings).length} strings from ` + `'${originalAppPath}' resources for '${configMarker || 'default'}' configuration`); } if (!outRoot) { return { apkStrings }; } const localPath = node_path_1.default.resolve(outRoot, 'strings.json'); await (0, support_1.mkdirp)(outRoot); await support_1.fs.writeFile(localPath, JSON.stringify(apkStrings, null, 2), 'utf-8'); return { apkStrings, localPath }; } /** * Get the package info from local apk file. * * @param appPath - The full path to existing .apk(s) package on the local * file system. * @returns The parsed application information. */ async function getApkInfo(appPath) { if (!(await support_1.fs.exists(appPath))) { throw new Error(`The file at path ${appPath} does not exist or is not accessible`); } if (appPath.endsWith(helpers_1.APKS_EXTENSION)) { appPath = await this.extractBaseApk(appPath); } try { const { name, versionCode, versionName } = await helpers_1.readPackageManifest.bind(this)(appPath); return { name, versionCode, versionName, }; } catch (e) { const err = e; logger_1.log.warn(`Error '${err.message}' while getting badging info`); } return {}; } // #region Private functions /** * Formats the config marker, which is then passed to parse.. methods * to make it compatible with resource formats generated by aapt(2) tool * * @param configsGetter The function whose result is a list * of apk configs * @param desiredMarker The desired config marker value * @param defaultMarker The default config marker value * @returns The formatted config marker */ async function formatConfigMarker(configsGetter, desiredMarker, defaultMarker) { let configMarker = desiredMarker || defaultMarker; if (configMarker.includes('-') && !configMarker.includes('-r')) { configMarker = configMarker.replace('-', '-r'); } const configs = await configsGetter(); logger_1.log.debug(`Resource configurations: ${JSON.stringify(configs)}`); // Assume the 'en' configuration is the default one if (configMarker.toLowerCase().startsWith('en') && !configs.some((x) => x.trim() === configMarker)) { logger_1.log.debug(`Resource configuration name '${configMarker}' is unknown. ` + `Replacing it with '${defaultMarker}'`); configMarker = defaultMarker; } else { logger_1.log.debug(`Selected configuration: '${configMarker}'`); } return configMarker; } /** * Parses apk strings from aapt2 tool output * * @param rawOutput The actual tool output * @param configMarker The config marker. Usually * a language abbreviation or an empty string for the default one * @returns Strings ids to values mapping. Plural * values are represented as arrays. If no config found for the * given marker then an empty mapping is returned. */ function parseAapt2Strings(rawOutput, configMarker) { const allLines = rawOutput.split(node_os_1.default.EOL); function extractContent(startIdx) { let idx = startIdx; const startCharPos = allLines[startIdx].indexOf('"'); if (startCharPos < 0) { return [null, idx]; } let result = ''; while (idx < allLines.length) { const terminationCharMatch = /"$/.exec(allLines[idx]); if (terminationCharMatch) { const terminationCharPos = terminationCharMatch.index; if (startIdx === idx) { return [allLines[idx].substring(startCharPos + 1, terminationCharPos), idx]; } return [`${result}\\n${lodash_1.default.trimStart(allLines[idx].substring(0, terminationCharPos))}`, idx]; } if (idx > startIdx) { result += `\\n${lodash_1.default.trimStart(allLines[idx])}`; } else { result += allLines[idx].substring(startCharPos + 1); } ++idx; } return [result, idx]; } const apkStrings = {}; let currentResourceId = null; let isInPluralGroup = false; let isInCurrentConfig = false; let lineIndex = 0; while (lineIndex < allLines.length) { const trimmedLine = allLines[lineIndex].trim(); if (lodash_1.default.isEmpty(trimmedLine)) { ++lineIndex; continue; } if (['type', 'Package'].some((x) => trimmedLine.startsWith(x))) { currentResourceId = null; isInPluralGroup = false; isInCurrentConfig = false; ++lineIndex; continue; } if (trimmedLine.startsWith('resource')) { isInPluralGroup = false; currentResourceId = null; isInCurrentConfig = false; if (trimmedLine.includes('string/')) { const match = /string\/(\S+)/.exec(trimmedLine); if (match) { currentResourceId = match[1]; } } else if (trimmedLine.includes('plurals/')) { const match = /plurals\/(\S+)/.exec(trimmedLine); if (match) { currentResourceId = match[1]; isInPluralGroup = true; } } ++lineIndex; continue; } if (currentResourceId) { if (isInPluralGroup) { if (trimmedLine.startsWith('(')) { isInCurrentConfig = trimmedLine.startsWith(`(${configMarker})`); ++lineIndex; continue; } if (isInCurrentConfig) { const [content, idx] = extractContent(lineIndex); lineIndex = idx; if (lodash_1.default.isString(content)) { apkStrings[currentResourceId] = [ ...(Array.isArray(apkStrings[currentResourceId]) ? apkStrings[currentResourceId] : []), content, ]; } } } else if (trimmedLine.startsWith(`(${configMarker})`)) { const [content, idx] = extractContent(lineIndex); lineIndex = idx; if (lodash_1.default.isString(content)) { apkStrings[currentResourceId] = content; } currentResourceId = null; } } ++lineIndex; } return apkStrings; } /** * Parses apk strings from aapt tool output * * @param rawOutput The actual tool output * @param configMarker The config marker. Usually * a language abbreviation or `(default)` * @returns Strings ids to values mapping. Plural * values are represented as arrays. If no config found for the * given marker then an empty mapping is returned. */ function parseAaptStrings(rawOutput, configMarker) { const normalizeStringMatch = function (s) { return s.replace(/"$/, '').replace(/^"/, '').replace(/\\"/g, '"'); }; const apkStrings = {}; let isInConfig = false; let currentResourceId = null; let isInPluralGroup = false; // The pattern matches any quoted content including escaped quotes const quotedStringPattern = /"[^"\\]*(?:\\.[^"\\]*)*"/; for (const line of rawOutput.split(node_os_1.default.EOL)) { const trimmedLine = line.trim(); if (lodash_1.default.isEmpty(trimmedLine)) { continue; } if (['config', 'type', 'spec', 'Package'].some((x) => trimmedLine.startsWith(x))) { isInConfig = trimmedLine.startsWith(`config ${configMarker}:`); currentResourceId = null; isInPluralGroup = false; continue; } if (!isInConfig) { continue; } if (trimmedLine.startsWith('resource')) { isInPluralGroup = false; currentResourceId = null; if (trimmedLine.includes(':string/')) { const match = /:string\/(\S+):/.exec(trimmedLine); if (match) { currentResourceId = match[1]; } } else if (trimmedLine.includes(':plurals/')) { const match = /:plurals\/(\S+):/.exec(trimmedLine); if (match) { currentResourceId = match[1]; isInPluralGroup = true; } } continue; } if (currentResourceId && trimmedLine.startsWith('(string')) { const match = quotedStringPattern.exec(trimmedLine); if (match) { apkStrings[currentResourceId] = normalizeStringMatch(match[0]); } currentResourceId = null; continue; } if (currentResourceId && isInPluralGroup && trimmedLine.includes(': (string')) { const match = quotedStringPattern.exec(trimmedLine); if (match) { apkStrings[currentResourceId] = [ ...(Array.isArray(apkStrings[currentResourceId]) ? apkStrings[currentResourceId] : []), normalizeStringMatch(match[0]), ]; } continue; } } return apkStrings; } // #endregion //# sourceMappingURL=apk-utils.js.map