Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce apks support #367

Merged
merged 13 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 62 additions & 39 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import path from 'path';
import { system, fs, zip, util } from 'appium-support';
import log from './logger.js';
import { exec } from 'teen_process';
import _ from 'lodash';
import B from 'bluebird';
import semver from 'semver';

const ZIP_MAGIC = 'PK';

const rootDir = path.resolve(__dirname, process.env.NO_PRECOMPILE ? '..' : '../..');
const APKS_EXTENSION = '.apks';
const APK_EXTENSION = '.apk';
const APK_INSTALL_TIMEOUT = 60000;
const APKS_INSTALL_TIMEOUT = APK_INSTALL_TIMEOUT * 2;

/**
* @typedef {Object} PlatformInfo
Expand Down Expand Up @@ -48,7 +51,7 @@ async function getAndroidPlatformAndPath () {
}
if (_.isEmpty(platformsMapping)) {
log.warn(`Found zero platform folders at '${path.resolve(androidHome, 'platforms')}'. ` +
`Do you have any Android SDKs installed?`);
`Do you have any Android SDKs installed?`);
return {
platform: null,
platformPath: null,
Expand All @@ -61,39 +64,11 @@ async function getAndroidPlatformAndPath () {
return result;
}

async function unzipFile (zipPath) {
log.debug(`Unzipping ${zipPath}`);
try {
await assertZipArchive(zipPath);
if (system.isWindows()) {
await zip.extractAllTo(zipPath, path.dirname(zipPath));
log.debug("Unzip successful");
} else {
await exec('unzip', ['-o', zipPath], {cwd: path.dirname(zipPath)});
log.debug("Unzip successful");
}
} catch (e) {
throw new Error(`Error occurred while unzipping. Original error: ${e.message}`);
}
}

async function assertZipArchive (zipPath) {
log.debug(`Testing zip archive: '${zipPath}'`);
if (!await fs.exists(zipPath)) {
throw new Error(`Zip archive does not exist at '${zipPath}'`);
}

const {size} = await fs.stat(zipPath);
if (size < 4) {
throw new Error(`The file at '${zipPath}' is too small to be a ZIP archive`);
}
const fd = await fs.open(zipPath, 'r');
const buffer = Buffer.alloc(ZIP_MAGIC.length);
await fs.read(fd, buffer, 0, ZIP_MAGIC.length, 0);
if (buffer.toString('ascii') !== ZIP_MAGIC) {
throw new Error(`The file signature '${buffer.toString('ascii')}' of '${zipPath}' ` +
`is not equal to the expected ZIP archive signature '${ZIP_MAGIC}'`);
}
async function unzipFile (zipPath, dstRoot = path.dirname(zipPath)) {
log.debug(`Unzipping '${zipPath}' to '${dstRoot}'`);
await zip.assertValidZip(zipPath);
await zip.extractAllTo(zipPath, dstRoot);
log.debug("Unzip successful");
}

function getIMEListFromOutput (stdout) {
Expand Down Expand Up @@ -307,7 +282,7 @@ const getBuildToolsDirs = _.memoize(async function getBuildToolsDirs (sdkRoot) {
.map((pair) => pair[1]);
} catch (err) {
log.warn(`Cannot sort build-tools folders ${JSON.stringify(buildToolsDirs.map((dir) => path.basename(dir)))} ` +
`by semantic version names.`);
`by semantic version names.`);
log.warn(`Falling back to sorting by modification date. Original error: ${err.message}`);
const pairs = await B.map(buildToolsDirs, async (dir) => [(await fs.stat(dir)).mtime.valueOf(), dir]);
buildToolsDirs = pairs
Expand Down Expand Up @@ -376,10 +351,58 @@ const extractMatchingPermissions = function (dumpsysOutput, groupNames, grantedS
return filteredResult;
};

/**
* @typedef {Object} InstallOptions
* @property {boolean} allowTestPackages [false] - Set to true in order to allow test
* packages installation.
* @property {boolean} useSdcard [false] - Set to true to install the app on sdcard
* instead of the device memory.
* @property {boolean} grantPermissions [false] - Set to true in order to grant all the
* permissions requested in the application's manifest
* automatically after the installation is completed
* under Android 6+.
* @property {boolean} replace [true] - Set it to false if you don't want
* the application to be upgraded/reinstalled
* if it is already present on the device.
*/

/**
* Transforms given options into the list of `adb install.install-multiple` command arguments
*
* @param {number} apiLevel - The current API level
* @param {?InstallOptions} options - The options mapping to transform
* @returns {Array<String>} The array of arguments
*/
function buildInstallArgs (apiLevel, options = {}) {
const result = [];

if (!util.hasValue(options.replace) || options.replace) {
result.push('-r');
}
if (options.allowTestPackages) {
result.push('-t');
}
if (options.useSdcard) {
result.push('-s');
}
if (options.grantPermissions) {
if (apiLevel < 23) {
log.debug(`Skipping permissions grant option, since ` +
`the current API level ${apiLevel} does not support applications ` +
`permissions customization`);
} else {
result.push('-g');
}
}

return result;
}

export {
getAndroidPlatformAndPath, unzipFile, assertZipArchive,
getAndroidPlatformAndPath, unzipFile,
getIMEListFromOutput, getJavaForOs, isShowingLockscreen, isCurrentFocusOnKeyguard,
getSurfaceOrientation, isScreenOnFully, buildStartCmd, getJavaHome,
rootDir, getSdkToolsVersion, getApksignerForOs, getBuildToolsDirs,
getApkanalyzerForOs, getOpenSslForOs, extractMatchingPermissions,
getApkanalyzerForOs, getOpenSslForOs, extractMatchingPermissions, APKS_EXTENSION,
APK_INSTALL_TIMEOUT, APKS_INSTALL_TIMEOUT, buildInstallArgs, APK_EXTENSION,
};
13 changes: 13 additions & 0 deletions lib/tools/adb-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ methods.initZipAlign = async function () {
this.binaries.zipalign = await this.getSdkBinaryPath("zipalign");
};

/**
* Get the full path to bundletool binary and assign it to
* this.binaries.bundletool property
*/
methods.initBundletool = async function () {
try {
this.binaries.bundletool = await fs.which('bundletool.jar');
} catch (err) {
throw new Error('bundletool.jar binary is expected to be present in PATH. ' +
'Visit https://github.com/google/bundletool for more details.');
}
};

/**
* Retrieve the API level of the device under test.
*
Expand Down
68 changes: 43 additions & 25 deletions lib/tools/android-manifest.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { exec } from 'teen_process';
import log from '../logger.js';
import { getAndroidPlatformAndPath, unzipFile, assertZipArchive,
getApkanalyzerForOs } from '../helpers.js';
import { fs } from 'appium-support';
import {
getAndroidPlatformAndPath, unzipFile,
getApkanalyzerForOs, APKS_EXTENSION } from '../helpers.js';
import { fs, zip } from 'appium-support';
import _ from 'lodash';
import path from 'path';
import xmldom from 'xmldom';
Expand Down Expand Up @@ -148,20 +149,24 @@ async function extractApkInfoWithApkanalyzer (localApk, apkanalyzerPath) {
/**
* Extract package and main activity name from application manifest.
*
* @param {string} localApk - The full path to application package.
* @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 (localApk) {
manifestMethods.packageAndLaunchActivityFromManifest = async function (appPath) {
if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

const apkInfoGetters = [
async () => {
const apkanalyzerPath = await getApkanalyzerForOs(this);
return await extractApkInfoWithApkanalyzer(localApk, apkanalyzerPath);
return await extractApkInfoWithApkanalyzer(appPath, apkanalyzerPath);
},
async () => {
await this.initAapt();
return await extractApkInfoWithApkTools(localApk,
return await extractApkInfoWithApkTools(appPath,
this.binaries.aapt, this.jars['appium_apk_tools.jar'], this.tmpDir);
},
];
Expand All @@ -175,29 +180,32 @@ manifestMethods.packageAndLaunchActivityFromManifest = async function (localApk)
return {apkPackage, apkActivity};
} catch (e) {
if (infoGetter !== _.last(apkInfoGetters)) {
log.info(`Using the alternative activity name detection method ` +
`because of: ${e.message}`);
log.info(`Using the alternative activity name detection method because of: ${e.message}`);
}
savedError = e;
}
}
throw new Error(`packageAndLaunchActivityFromManifest failed. ` +
`Original error: ${savedError.message}` +
throw new Error(`packageAndLaunchActivityFromManifest failed. Original error: ${savedError.message}` +
(savedError.stderr ? `; StdErr: ${savedError.stderr}` : ''));
};

/**
* Extract target SDK version from application manifest.
*
* @param {string} localApk - The full path to application package.
* @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 (localApk) {
manifestMethods.targetSdkVersionFromManifest = async function (appPath) {
await this.initAapt();

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log.info("Extracting package and launch activity from manifest");
let args = ['dump', 'badging', localApk];
let args = ['dump', 'badging', appPath];
let output;
try {
let {stdout} = await exec(this.binaries.aapt, args);
Expand Down Expand Up @@ -285,7 +293,7 @@ manifestMethods.insertManifest = async function (manifest, srcApk, dstApk) {
await unzipFile(`${manifest}.apk`);
await fs.copyFile(srcApk, dstApk);
log.debug("Testing new tmp apk");
await assertZipArchive(dstApk);
await zip.assertValidZip(dstApk);
log.debug("Moving manifest");
try {
await exec(this.binaries.aapt, [
Expand All @@ -301,32 +309,42 @@ manifestMethods.insertManifest = async function (manifest, srcApk, dstApk) {
/**
* Check whether package manifest contains Internet permissions.
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to .apk(s) package.
* @return {boolean} True if the manifest requires Internet access permission.
*/
manifestMethods.hasInternetPermissionFromManifest = async function (localApk) {
manifestMethods.hasInternetPermissionFromManifest = async function (appPath) {
await this.initAapt();
log.debug(`Checking if '${localApk}' requires internet access permission in the manifest`);

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log.debug(`Checking if '${appPath}' requires internet access permission in the manifest`);
try {
let {stdout} = await exec(this.binaries.aapt, ['dump', 'badging', localApk]);
let {stdout} = await exec(this.binaries.aapt, ['dump', 'badging', appPath]);
return new RegExp(/uses-permission:.*'android.permission.INTERNET'/).test(stdout);
} catch (e) {
throw new Error(`Cannot check if '${localApk}' requires internet access permission. ` +
throw new Error(`Cannot check if '${appPath}' requires internet access permission. ` +
`Original error: ${e.message}`);
}
};

/*
/**
* Prints out the manifest extracted from the apk
*
* @param {string} localApk - The full path to application package.
* @param {string} appPath - The full path to application package.
* @param {?string} logLevel - The level at which to log. E.g., 'debug'
*/
manifestMethods.printManifestFromApk = async function printManifestFromApk (localApk, logLevel = 'debug') {
manifestMethods.printManifestFromApk = async function printManifestFromApk (appPath, logLevel = 'debug') {
await this.initAapt();
log[logLevel](`Android manifest extracted from '${localApk}'`);

if (appPath.endsWith(APKS_EXTENSION)) {
appPath = await this.extractBaseApk(appPath);
}

log[logLevel](`Extracting the manifest from '${appPath}'`);
let out = false;
const {stdout} = await exec(this.binaries.aapt, ['l', '-a', localApk]);
const {stdout} = await exec(this.binaries.aapt, ['l', '-a', appPath]);
for (const line of stdout.split('\n')) {
if (!out && line.includes('Android manifest:')) {
out = true;
Expand Down
28 changes: 21 additions & 7 deletions lib/tools/apk-signing.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { exec } from 'teen_process';
import path from 'path';
import log from '../logger.js';
import { tempDir, system, mkdirp, fs, zip } from 'appium-support';
import { getJavaForOs, getApksignerForOs, getJavaHome, rootDir } from '../helpers.js';
import { getJavaForOs, getApksignerForOs, getJavaHome, rootDir, APKS_EXTENSION } from '../helpers.js';

const DEFAULT_PRIVATE_KEY = path.resolve(rootDir, 'keys', 'testkey.pk8');
const DEFAULT_CERTIFICATE = path.resolve(rootDir, 'keys', 'testkey.x509.pem');
const DEFAULT_CERT_DIGEST = 'a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dc';
const BUNDLETOOL_TUTORIAL = 'https://developer.android.com/studio/command-line/bundletool';
const APKSIGNER_VERIFY_FAIL = 'DOES NOT VERIFY';

let apkSigningMethods = {};
Expand Down Expand Up @@ -165,10 +166,23 @@ apkSigningMethods.signWithCustomCert = async function (apk) {
* custom or default certificate based on _this.useKeystore_ property value
* and Zip-aligns it after signing.
*
* @param {string} apk - The full path to the local apk file.
* @param {string} appPath - The full path to the local .apk(s) file.
* @throws {Error} If signing fails.
*/
apkSigningMethods.sign = async function (apk) {
apkSigningMethods.sign = async function (appPath) {
if (appPath.endsWith(APKS_EXTENSION)) {
let message = 'Signing of .apks-files is not supported. ';
if (this.useKeystore) {
message += 'Consider manual application bundle signing with the custom keystore ' +
`like it is described at ${BUNDLETOOL_TUTORIAL}`;
} else {
message += `Consider manual application bundle signing with the key at '${DEFAULT_PRIVATE_KEY}' ` +
`and the certificate at '${DEFAULT_CERTIFICATE}'. Read ${BUNDLETOOL_TUTORIAL} for more details.`;
}
log.warn(message);
return;
}

let apksignerFound = true;
try {
await getApksignerForOs(this);
Expand All @@ -180,17 +194,17 @@ apkSigningMethods.sign = async function (apk) {
// it is necessary to apply zipalign only before signing
// if apksigner is used or only after signing if we only have
// sign.jar utility
await this.zipAlignApk(apk);
await this.zipAlignApk(appPath);
}

if (this.useKeystore) {
await this.signWithCustomCert(apk);
await this.signWithCustomCert(appPath);
} else {
await this.signWithDefaultCert(apk);
await this.signWithDefaultCert(appPath);
}

if (!apksignerFound) {
await this.zipAlignApk(apk);
await this.zipAlignApk(appPath);
}
};

Expand Down
Loading