Skip to content

Commit

Permalink
feat: Use aapt2 instead of ApkReader (#757)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jul 10, 2024
1 parent 9a1bf69 commit 8efcf5b
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 191 deletions.
205 changes: 150 additions & 55 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import _ from 'lodash';
import B from 'bluebird';
import semver from 'semver';
import os from 'os';
import { exec } from 'teen_process';

const APKS_EXTENSION = '.apks';
const APK_EXTENSION = '.apk';
Expand Down Expand Up @@ -600,60 +601,6 @@ function buildInstallArgs (apiLevel, options = {}) {

return result;
}

/**
* @typedef {Object} ManifestInfo
* @property {string} pkg - The application identifier
* @property {string} [activity] - The name of the main package activity
* @property {?number} versionCode - The version code number (might be `NaN`)
* @property {?string} versionName - The version name (might be `null`)
*/

/**
* Perform parsing of the manifest object in order
* to extract some vital values from it
*
* @param {Record<string, any>} manifest The manifest content formatted as JSON
* See https://www.npmjs.com/package/@devicefarmer/adbkit-apkreader for detailed format description
* @returns {ManifestInfo}
*/
function parseManifest (manifest) {
const result = {
pkg: manifest.package,
versionCode: parseInt(manifest.versionCode, 10),
versionName: manifest.versionName || null,
};
if (!manifest.application) {
return result;
}

// Look for enabled activity or activity-alias with
// action == android.intent.action.MAIN and
// category == android.intent.category.LAUNCHER
for (const activity of [
...manifest.application.activities,
...manifest.application.activityAliases,
]) {
if (activity.enabled === false || _.isEmpty(activity.intentFilters)) {
continue;
}

for (const {actions, categories} of activity.intentFilters) {
if (_.isEmpty(actions) || _.isEmpty(categories)) {
continue;
}

const isMainAction = actions.some(({name}) => name === MAIN_ACTION);
const isLauncherCategory = categories.some(({name}) => name === LAUNCHER_CATEGORY);
if (isMainAction && isLauncherCategory) {
result.activity = activity.name;
return result;
}
}
}
return result;
}

/**
* Parses apk strings from aapt tool output
*
Expand Down Expand Up @@ -1061,14 +1008,162 @@ function matchComponentName (classString) {
return /^[a-z0-9./_]+$/i.exec(classString);
}

/**
* @typedef {Object} LaunchableActivity
* @property {string} name
* @property {string} [label]
* @property {string} [icon]
*/

/**
* @typedef {Object} ApkManifest
* @property {string} name Package name, for example 'io.appium.android.apis'
* @property {number} versionCode
* @property {string} [versionName]
* @property {string} [platformBuildVersionName]
* @property {number} [platformBuildVersionCode]
* @property {number} compileSdkVersion
* @property {string} [compileSdkVersionCodename]
* @property {number} minSdkVersion
* @property {number} [targetSdkVersion]
* @property {string[]} usesPermissions List of requested permissions
* @property {LaunchableActivity} launchableActivity
* @property {string[]} locales List of supported locales
* @property {string[]} architectures List of supported architectures. Could be empty for older apps.
* @property {number[]} densities List of supported display densities
*/

/**
* Extracts various package manifest details
* from the given application file.
*
* @this {import('./adb.js').ADB}
* @param {string} apkPath Full path to the application file.
* @returns {Promise<ApkManifest>}
*/
export async function readPackageManifest(apkPath) {
await this.initAapt2();
const aapt2Binary = (/** @type {import('@appium/types').StringRecord} */ (this.binaries)).aapt2;

const args = ['dump', 'badging', apkPath];
log.debug(`Reading package manifest: '${util.quote([aapt2Binary, ...args])}'`);
/** @type {string} */
let stdout;
try {
({stdout} = await exec(aapt2Binary, args));
} catch (e) {
const prefix = `Cannot read the manifest from '${apkPath}'`;
const suffix = `Original error: ${e.stderr || e.message}`;
if (_.includes(e.stderr, `Unable to open 'badging'`)) {
throw new Error(`${prefix}. Update build tools to use a newer aapt2 version. ${suffix}`);
}
throw new Error(`${prefix}. ${suffix}`);
}

const extractValue = (
/** @type {string} */ line,
/** @type {RegExp} */ propPattern,
/** @type {((x: string) => any)|undefined} */ valueTransformer
) => {
const match = propPattern.exec(line);
if (match) {
return valueTransformer ? valueTransformer(match[1]) : match[1];
}
};
const extractArray = (
/** @type {string} */ line,
/** @type {RegExp} */ propPattern,
/** @type {((x: string) => any)|undefined} */ valueTransformer
) => {
let match;
const resultArray = [];
while ((match = propPattern.exec(line))) {
resultArray.push(valueTransformer ? valueTransformer(match[1]) : match[1]);
}
return resultArray;
};

const toInt = (/** @type {string} */ x) => parseInt(x, 10);

/** @type {ApkManifest} */
const result = {
name: '',
versionCode: 0,
minSdkVersion: 0,
compileSdkVersion: 0,
usesPermissions: [],
launchableActivity: {
name: '',
},
architectures: [],
locales: [],
densities: [],
};
for (const line of stdout.split('\n')) {
if (line.startsWith('package:')) {
for (const [name, pattern, transformer] of [
['name', /name='([^']+)'/],
['versionCode', /versionCode='([^']+)'/, toInt],
['versionName', /versionName='([^']+)'/],
['platformBuildVersionName', /platformBuildVersionName='([^']+)'/],
['platformBuildVersionCode', /platformBuildVersionCode='([^']+)'/, toInt],
['compileSdkVersion', /compileSdkVersion='([^']+)'/, toInt],
['compileSdkVersionCodename', /compileSdkVersionCodename='([^']+)'/],
]) {
const value = extractValue(
line,
/** @type {RegExp} */ (pattern),
/** @type {((x: string) => any)|undefined} */ (transformer)
);
if (!_.isUndefined(value)) {
result[/** @type {string} */ (name)] = value;
}
}
} else if (line.startsWith('sdkVersion:') || line.startsWith('minSdkVersion:')) {
const value = extractValue(line, /[sS]dkVersion:'([^']+)'/, toInt);
if (value) {
result.minSdkVersion = value;
}
} else if (line.startsWith('targetSdkVersion:')) {
const value = extractValue(line, /targetSdkVersion:'([^']+)'/, toInt);
if (value) {
result.targetSdkVersion = value;
}
} else if (line.startsWith('uses-permission:')) {
const value = extractValue(line, /name='([^']+)'/);
if (value) {
result.usesPermissions.push(/** @type {string} */ (value));
}
} else if (line.startsWith('launchable-activity:')) {
for (const [name, pattern] of [
['name', /name='([^']+)'/],
['label', /label='([^']+)'/],
['icon', /icon='([^']+)'/],
]) {
const value = extractValue(line, /** @type {RegExp} */ (pattern));
if (value) {
result.launchableActivity[/** @type {string} */ (name)] = value;
}
}
} else if (line.startsWith('locales:')) {
result.locales = /** @type {string[]} */ (extractArray(line, /'([^']+)'/g));
} else if (line.startsWith('native-code:')) {
result.architectures = /** @type {string[]} */ (extractArray(line, /'([^']+)'/g));
} else if (line.startsWith('densities:')) {
result.densities = /** @type {number[]} */ (extractArray(line, /'([^']+)'/g, toInt));
}
}
return result;
}

export {
getAndroidPlatformAndPath, unzipFile,
getIMEListFromOutput, getJavaForOs, isShowingLockscreen, isCurrentFocusOnKeyguard,
getSurfaceOrientation, isScreenOnFully, buildStartCmd, getJavaHome,
getSdkToolsVersion, getApksignerForOs, getBuildToolsDirs,
getApkanalyzerForOs, getOpenSslForOs, extractMatchingPermissions, APKS_EXTENSION,
APK_INSTALL_TIMEOUT, APKS_INSTALL_TIMEOUT, buildInstallArgs, APK_EXTENSION,
DEFAULT_ADB_EXEC_TIMEOUT, parseManifest, parseAaptStrings, parseAapt2Strings,
DEFAULT_ADB_EXEC_TIMEOUT, parseAaptStrings, parseAapt2Strings,
formatConfigMarker, unsignApk, toAvdLocaleArgs, requireSdkRoot,
getSdkRootFromEnv, getAndroidPrefsRoot, dirExists, escapeShellArg,
parseLaunchableActivityNames, matchComponentName, getResourcePath
Expand Down
3 changes: 0 additions & 3 deletions lib/stubs.ts

This file was deleted.

75 changes: 19 additions & 56 deletions lib/tools/android-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import { exec } from 'teen_process';
import log from '../logger.js';
import {
getAndroidPlatformAndPath, unzipFile,
APKS_EXTENSION, parseManifest
APKS_EXTENSION, readPackageManifest,
} from '../helpers.js';
import { fs, zip, tempDir, util } from '@appium/support';
import _ from 'lodash';
import path from 'path';
import ApkReader from '@devicefarmer/adbkit-apkreader';

const manifestMethods = {};

Expand All @@ -31,22 +29,15 @@ manifestMethods.packageAndLaunchActivityFromManifest = async function packageAnd
appPath = await this.extractBaseApk(appPath);
}

let manifest;
try {
const apkReader = await ApkReader.open(appPath);
manifest = await apkReader.readManifest();
} catch (e) {
log.debug(e);
throw new Error(`Cannot extract a manifest from '${appPath}'. ` +
`Is it a valid Android application?`);
}
const {pkg, activity} = parseManifest(manifest);
log.info(`Package name: '${pkg}'`);
log.info(`Main activity name: '${activity}'`);
return {
apkPackage: pkg,
apkActivity: activity,
};
const {
name: apkPackage,
launchableActivity: {
name: apkActivity,
},
} = await readPackageManifest.bind(this)(appPath);
log.info(`Package name: '${apkPackage}'`);
log.info(`Main activity name: '${apkActivity}'`);
return {apkPackage, apkActivity};
};

/**
Expand All @@ -65,41 +56,14 @@ manifestMethods.targetSdkVersionFromManifest = async function targetSdkVersionFr
appPath = await this.extractBaseApk(appPath);
}

const getTargetSdkViaApkReader = async () => {
const apkReader = await ApkReader.open(appPath);
const manifest = await apkReader.readManifest();
if (manifest.usesSdk && _.isInteger(manifest.usesSdk.targetSdkVersion)) {
return manifest.usesSdk.targetSdkVersion;
}
throw new Error('Cannot find the information about targetSdkVersion in the manifest');
};
const getTargetSdkViaAapt = async () => {
await this.initAapt();
const args = ['dump', 'badging', appPath];
const {stdout} = await exec((/** @type {import('@appium/types').StringRecord}*/ (this.binaries)).aapt, args);
const targetSdkVersion = /targetSdkVersion:'([^']+)'/g.exec(stdout);
if (!targetSdkVersion) {
log.debug(stdout);
throw new Error('Cannot parse the command output');
}
return parseInt(targetSdkVersion[1], 10);
};
/** @type {[string, () => Promise<number>][]} */
const versionGetters = [
['ApkReader', getTargetSdkViaApkReader],
['aapt', getTargetSdkViaAapt],
];
for (const [toolName, versionGetter] of versionGetters) {
try {
return await versionGetter();
} catch (e) {
log.info(`Cannot extract targetSdkVersion of '${originalAppPath}' using ${toolName}. ` +
`Original error: ${e.message}`);
}
const {targetSdkVersion} = await readPackageManifest.bind(this)(appPath);
if (!targetSdkVersion) {
throw new Error(
`Cannot extract targetSdkVersion of '${originalAppPath}'. Does ` +
`the package manifest define it?`
);
}
throw new Error(`Cannot extract the target SDK version number of '${originalAppPath}' using either of ` +
`${JSON.stringify(versionGetters.map((pair) => pair[0]))} tools. ` +
`Check the server log for more details`);
return targetSdkVersion;
};

/**
Expand Down Expand Up @@ -249,9 +213,8 @@ manifestMethods.hasInternetPermissionFromManifest = async function hasInternetPe
appPath = await this.extractBaseApk(appPath);
}

const apkReader = await ApkReader.open(appPath);
const manifest = await apkReader.readManifest();
return (manifest.usesPermissions || []).some(({name}) => name === 'android.permission.INTERNET');
const {usesPermissions} = await readPackageManifest.bind(this)(appPath);
return usesPermissions.some((/** @type {string} */ name) => name === 'android.permission.INTERNET');
};

export default manifestMethods;
Expand Down
11 changes: 4 additions & 7 deletions lib/tools/apk-utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
buildStartCmd, APKS_EXTENSION, buildInstallArgs,
APK_INSTALL_TIMEOUT, DEFAULT_ADB_EXEC_TIMEOUT,
parseManifest, parseAaptStrings, parseAapt2Strings, formatConfigMarker,
escapeShellArg,
parseAaptStrings, parseAapt2Strings, formatConfigMarker,
escapeShellArg, readPackageManifest
} from '../helpers.js';
import { exec } from 'teen_process';
import log from '../logger.js';
Expand All @@ -13,7 +13,6 @@ import { fs, util, mkdirp, timing } from '@appium/support';
import semver from 'semver';
import os from 'os';
import { LRUCache } from 'lru-cache';
import ApkReader from '@devicefarmer/adbkit-apkreader';

const apkUtilsMethods = {};

Expand Down Expand Up @@ -1108,11 +1107,9 @@ apkUtilsMethods.getApkInfo = async function getApkInfo (appPath) {
}

try {
const apkReader = await ApkReader.open(appPath);
const manifest = await apkReader.readManifest();
const {pkg, versionName, versionCode} = parseManifest(manifest);
const {name, versionCode, versionName} = await readPackageManifest.bind(this)(appPath);
return {
name: pkg,
name,
versionCode,
versionName,
};
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"homepage": "https://github.com/appium/appium-adb",
"dependencies": {
"@appium/support": "^5.0.3",
"@devicefarmer/adbkit-apkreader": "^3.2.4",
"async-lock": "^1.0.0",
"asyncbox": "^3.0.0",
"bluebird": "^3.4.7",
Expand Down
Loading

0 comments on commit 8efcf5b

Please sign in to comment.