android-manifest.js
9.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import { exec } from 'teen_process';
import log from '../logger.js';
import {
getAndroidPlatformAndPath, unzipFile,
APKS_EXTENSION, parseManifest } from '../helpers.js';
import { fs, zip, tempDir } from 'appium-support';
import _ from 'lodash';
import path from 'path';
import { quote } from 'shell-quote';
import ApkReader from 'adbkit-apkreader';
let manifestMethods = {};
/**
* @typedef {Object} APKInfo
* @property {string} apkPackage - The name of application package, for example 'com.acme.app'.
* @property {string} apkActivity - The name of main application activity.
*/
/**
* Extract package and main activity name from application manifest.
*
* @param {string} appPath - The full path to application .apk(s) package
* @return {APKInfo} The parsed application info.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
manifestMethods.packageAndLaunchActivityFromManifest = async function packageAndLaunchActivityFromManifest (appPath) {
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}
const apkReader = await ApkReader.open(appPath);
const manifest = await apkReader.readManifest();
const {pkg, activity} = parseManifest(manifest);
log.info(`Package name: '${pkg}'`);
log.info(`Main activity name: '${activity}'`);
return {
apkPackage: pkg,
apkActivity: activity,
};
};
/**
* Extract target SDK version from application manifest.
*
* @param {string} appPath - The full path to .apk(s) package.
* @return {number} The version of the target SDK.
* @throws {error} If there was an error while getting the data from the given
* application package.
*/
manifestMethods.targetSdkVersionFromManifest = async function targetSdkVersionFromManifest (appPath) {
log.debug(`Extracting target SDK version of '${appPath}'`);
const originalAppPath = appPath;
if (appPath.endsWith(APKS_EXTENSION)) {
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(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);
};
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}`);
}
}
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`);
};
/**
* Extract target SDK version from package information.
*
* @param {string} pkg - The class name of the package installed on the device under test.
* @param {?string} cmdOutput - Optional parameter containing the output of
* _dumpsys package_ command. It may speed up the method execution.
* @return {number} The version of the target SDK.
*/
manifestMethods.targetSdkVersionUsingPKG = async function targetSdkVersionUsingPKG (pkg, cmdOutput = null) {
let stdout = cmdOutput || await this.shell(['dumpsys', 'package', pkg]);
let targetSdkVersion = new RegExp(/targetSdk=([^\s\s]+)/g).exec(stdout);
if (targetSdkVersion && targetSdkVersion.length >= 2) {
targetSdkVersion = targetSdkVersion[1];
} else {
// targetSdk not found in the dump, assigning 0 to targetSdkVersion
targetSdkVersion = 0;
}
return parseInt(targetSdkVersion, 10);
};
/**
* 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 {string} manifest - Full path to the initial manifest template
* @param {string} manifestPackage - The name of the manifest package
* @param {string} targetPackage - The name of the destination package
*/
manifestMethods.compileManifest = async function compileManifest (manifest, manifestPackage, targetPackage) {
const {platform, platformPath} = await getAndroidPlatformAndPath();
if (!platform) {
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 args = [
'link',
'-o', resultPath,
'--manifest', manifest,
'--rename-manifest-package', manifestPackage,
'--rename-instrumentation-target-package', targetPackage,
'-I', androidJarPath,
'-v',
];
log.debug(`Compiling the manifest using '${quote([this.binaries.aapt2, ...args])}'`);
await exec(this.binaries.aapt2, args);
} catch (e) {
log.debug('Cannot compile the manifest using aapt2. Defaulting to aapt. ' +
`Original error: ${e.stderr || e.message}`);
await this.initAapt();
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 '${quote([this.binaries.aapt, ...args])}'`);
try {
await exec(this.binaries.aapt, args);
} catch (e1) {
throw new Error(`Cannot compile the manifest. Original error: ${e1.stderr || e1.message}`);
}
}
log.debug(`Compiled the manifest at '${resultPath}'`);
};
/**
* Replace/insert the specially precompiled manifest file into the
* particular package.
*
* @param {string} manifest - Full path to the precompiled manifest
* created by `compileManifest` method call
* without .apk extension
* @param {string} srcApk - Full path to the existing valid application package, where
* this manifest has to be insetred to. This package
* will NOT be modified.
* @param {string} dstApk - Full path to the resulting package.
* The file will be overridden if it already exists.
*/
manifestMethods.insertManifest = async function insertManifest (manifest, srcApk, dstApk) {
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();
await fs.copyFile(srcApk, dstApk);
log.debug('Moving manifest');
try {
await exec(this.binaries.aapt, [
'remove', dstApk, manifestName
]);
} catch (ign) {}
await exec(this.binaries.aapt, [
'add', dstApk, manifestName
], {cwd: path.dirname(manifest)});
} catch (e) {
log.debug('Cannot insert manifest using aapt. Defaulting to zip. ' +
`Original error: ${e.stderr || e.message}`);
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 {string} appPath - The full path to .apk(s) package.
* @return {boolean} True if the manifest requires Internet access permission.
*/
manifestMethods.hasInternetPermissionFromManifest = async function hasInternetPermissionFromManifest (appPath) {
log.debug(`Checking if '${appPath}' requires internet access permission in the manifest`);
if (appPath.endsWith(APKS_EXTENSION)) {
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');
};
export default manifestMethods;