appium-adb
Version:
Android Debug Bridge interface
283 lines (269 loc) • 9.52 kB
text/typescript
import _ from 'lodash';
import {exec} from 'teen_process';
import {log} from '../logger';
import {unzipFile, APKS_EXTENSION, readPackageManifest} from '../helpers';
import {fs, zip, tempDir, util} from '@appium/support';
import path from 'node:path';
import type {ADB} from '../adb';
import type {APKInfo, PlatformInfo, StringRecord} from './types';
/**
* Extract package and main activity name from application manifest.
*
* @param appPath - The full path to application .apk(s) package
* @return The parsed application info.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
export async function packageAndLaunchActivityFromManifest(
this: ADB,
appPath: string,
): Promise<APKInfo> {
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}
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};
}
/**
* Extract target SDK version from application manifest.
*
* @param appPath - The full path to .apk(s) package.
* @return The version of the target SDK.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
export async function targetSdkVersionFromManifest(this: ADB, appPath: string): Promise<number> {
log.debug(`Extracting target SDK version of '${appPath}'`);
const originalAppPath = appPath;
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}
const {targetSdkVersion} = await readPackageManifest.bind(this)(appPath);
if (!targetSdkVersion) {
throw new Error(
`Cannot extract targetSdkVersion of '${originalAppPath}'. Does ` +
`the package manifest define it?`,
);
}
return targetSdkVersion;
}
/**
* Extract target SDK version from package information.
*
* @param pkg - The class name of the package installed on the device under test.
* @param cmdOutput - Optional parameter containing the output of
* _dumpsys package_ command. It may speed up the method execution.
* @return The version of the target SDK.
*/
export async function targetSdkVersionUsingPKG(
this: ADB,
pkg: string,
cmdOutput: string | null = null,
): Promise<number> {
const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg]));
const targetSdkVersionMatch = new RegExp(/targetSdk=([^\s\s]+)/g).exec(stdout);
return targetSdkVersionMatch && targetSdkVersionMatch.length >= 2
? parseInt(targetSdkVersionMatch[1], 10)
: 0;
}
/**
* Create binary representation of package manifest (usually AndroidManifest.xml).
* `${manifest}.apk` file will be created as the result of this method
* containing the compiled manifest.
*
* @param manifest - Full path to the initial manifest template
* @param manifestPackage - The name of the manifest package
* @param targetPackage - The name of the destination package
*/
export async function compileManifest(
this: ADB,
manifest: string,
manifestPackage: string,
targetPackage: string,
): Promise<void> {
const {platform, platformPath} = await getAndroidPlatformAndPath(this.sdkRoot as string);
if (!platform || !platformPath) {
throw new Error(
'Cannot compile the manifest. The required platform does not exist (API level >= 17)',
);
}
const resultPath = `${manifest}.apk`;
const androidJarPath = path.resolve(platformPath, 'android.jar');
if (await fs.exists(resultPath)) {
await fs.rimraf(resultPath);
}
try {
await this.initAapt2();
// https://developer.android.com/studio/command-line/aapt2
const binaries = this.binaries as StringRecord;
const args = [
'link',
'-o',
resultPath,
'--manifest',
manifest,
'--rename-manifest-package',
manifestPackage,
'--rename-instrumentation-target-package',
targetPackage,
'-I',
androidJarPath,
'-v',
];
log.debug(`Compiling the manifest using '${util.quote([binaries.aapt2, ...args])}'`);
await exec(binaries.aapt2, args);
} catch (e) {
log.debug(
'Cannot compile the manifest using aapt2. Defaulting to aapt. ' +
`Original error: ${(e as Error).message || (e as {stderr?: string}).stderr}`,
);
await this.initAapt();
const binaries = this.binaries as StringRecord;
const args = [
'package',
'-M',
manifest,
'--rename-manifest-package',
manifestPackage,
'--rename-instrumentation-target-package',
targetPackage,
'-I',
androidJarPath,
'-F',
resultPath,
'-f',
];
log.debug(`Compiling the manifest using '${util.quote([binaries.aapt, ...args])}'`);
try {
await exec(binaries.aapt, args);
} catch (e1) {
throw new Error(
`Cannot compile the manifest. Original error: ${(e1 as Error).message || (e1 as {stderr?: string}).stderr}`,
);
}
}
log.debug(`Compiled the manifest at '${resultPath}'`);
}
/**
* Replace/insert the specially precompiled manifest file into the
* particular package.
*
* @param manifest - Full path to the precompiled manifest
* created by `compileManifest` method call
* without .apk extension
* @param srcApk - Full path to the existing valid application package, where
* this manifest has to be insetred to. This package
* will NOT be modified.
* @param dstApk - Full path to the resulting package.
* The file will be overridden if it already exists.
*/
export async function insertManifest(
this: ADB,
manifest: string,
srcApk: string,
dstApk: string,
): Promise<void> {
log.debug(`Inserting manifest '${manifest}', src: '${srcApk}', dst: '${dstApk}'`);
await zip.assertValidZip(srcApk);
await unzipFile(`${manifest}.apk`);
const manifestName = path.basename(manifest);
try {
await this.initAapt();
const binaries = this.binaries as StringRecord;
await fs.copyFile(srcApk, dstApk);
log.debug('Moving manifest');
try {
await exec(binaries.aapt, ['remove', dstApk, manifestName]);
} catch {}
await exec(binaries.aapt, ['add', dstApk, manifestName], {
cwd: path.dirname(manifest),
});
} catch (e) {
log.debug(
'Cannot insert manifest using aapt. Defaulting to zip. ' +
`Original error: ${(e as Error).message || (e as {stderr?: string}).stderr}`,
);
const tmpRoot = await tempDir.openDir();
try {
// Unfortunately NodeJS does not provide any reliable methods
// to replace files inside zip archives without loading the
// whole archive content into RAM
log.debug(`Extracting the source apk at '${srcApk}'`);
await zip.extractAllTo(srcApk, tmpRoot);
log.debug('Moving manifest');
await fs.mv(manifest, path.resolve(tmpRoot, manifestName));
log.debug(`Collecting the destination apk at '${dstApk}'`);
await zip.toArchive(dstApk, {
cwd: tmpRoot,
});
} finally {
await fs.rimraf(tmpRoot);
}
}
log.debug(`Manifest insertion into '${dstApk}' is completed`);
}
/**
* Check whether package manifest contains Internet permissions.
*
* @param appPath - The full path to .apk(s) package.
* @return True if the manifest requires Internet access permission.
*/
export async function hasInternetPermissionFromManifest(
this: ADB,
appPath: string,
): Promise<boolean> {
log.debug(`Checking if '${appPath}' requires internet access permission in the manifest`);
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}
const {usesPermissions} = await readPackageManifest.bind(this)(appPath);
return usesPermissions.some((name: string) => name === 'android.permission.INTERNET');
}
// #region Private functions
/**
* Retrieve the path to the recent installed Android platform.
*
* @param sdkRoot
* @return The resulting path to the newest installed platform.
*/
export async function getAndroidPlatformAndPath(sdkRoot: string): Promise<PlatformInfo> {
const propsPaths = await fs.glob('*/build.prop', {
cwd: path.resolve(sdkRoot, 'platforms'),
absolute: true,
});
const platformsMapping: Record<string, PlatformInfo> = {};
for (const propsPath of propsPaths) {
const propsContent = await fs.readFile(propsPath, 'utf-8');
const platformPath = path.dirname(propsPath);
const platform = path.basename(platformPath);
const match = /ro\.build\.version\.sdk=(\d+)/.exec(propsContent);
if (!match) {
log.warn(`Cannot read the SDK version from '${propsPath}'. Skipping '${platform}'`);
continue;
}
platformsMapping[parseInt(match[1], 10)] = {
platform,
platformPath,
};
}
if (_.isEmpty(platformsMapping)) {
log.warn(
`Found zero platform folders at '${path.resolve(sdkRoot, 'platforms')}'. ` +
`Do you have any Android SDKs installed?`,
);
return {
platform: null,
platformPath: null,
};
}
const recentSdkVersion = _.keys(platformsMapping).sort().reverse()[0];
const result = platformsMapping[recentSdkVersion];
log.debug(`Found the most recent Android platform: ${JSON.stringify(result)}`);
return result;
}
// #endregion