Skip to content

Commit f801657

Browse files
authored
feat: install apk (#56)
1 parent ca9e51d commit f801657

File tree

5 files changed

+170
-7
lines changed

5 files changed

+170
-7
lines changed

src/commands/android/subcommands/common.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,8 @@ export async function showConnectedEmulators() {
6666
return false;
6767
}
6868
}
69+
70+
export function showMissingRequirementsHelp() {
71+
Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android --standalone')} to setup missing requirements.`);
72+
Logger.log(`(Remove the ${colors.gray('--standalone')} flag from the above command if setting up for testing.)\n`);
73+
}

src/commands/android/subcommands/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import colors from 'ansi-colors';
22
import * as dotenv from 'dotenv';
33
import path from 'path';
44

5-
import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common';
6-
import {connect} from './connect';
7-
import {getPlatformName} from '../../../utils';
85
import Logger from '../../../logger';
6+
import {getPlatformName} from '../../../utils';
97
import {Options, Platform} from '../interfaces';
8+
import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common';
9+
import {connect} from './connect';
10+
import {install} from './install';
1011

1112
export class AndroidSubcommand {
1213
sdkRoot: string;
@@ -56,6 +57,8 @@ export class AndroidSubcommand {
5657
async executeSubcommand(): Promise<boolean> {
5758
if (this.subcommand === 'connect') {
5859
return await connect(this.options, this.sdkRoot, this.platform);
60+
} else if (this.subcommand === 'install') {
61+
return await install(this.options, this.sdkRoot, this.platform);
5962
}
6063

6164
return false;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import colors from 'ansi-colors';
2+
import {existsSync} from 'fs';
3+
import inquirer from 'inquirer';
4+
import path from 'path';
5+
6+
import Logger from '../../../../logger';
7+
import {symbols} from '../../../../utils';
8+
import {Options, Platform} from '../../interfaces';
9+
import ADB from '../../utils/appium-adb';
10+
import {getBinaryLocation} from '../../utils/common';
11+
import {execBinaryAsync} from '../../utils/sdk';
12+
import {showMissingRequirementsHelp} from '../common';
13+
14+
export async function installApp(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
15+
try {
16+
const adbLocation = getBinaryLocation(sdkRoot, platform, 'adb', true);
17+
if (!adbLocation) {
18+
Logger.log(` ${colors.red(symbols().fail)} ${colors.cyan('adb')} binary not found.\n`);
19+
showMissingRequirementsHelp();
20+
21+
return false;
22+
}
23+
24+
const adb = await ADB.createADB({allowOfflineDevices: true});
25+
const devices = await adb.getConnectedDevices();
26+
27+
if (!devices.length) {
28+
Logger.log(`${colors.red('No device found running.')} Please connect a device to install the APK.`);
29+
Logger.log(`Use ${colors.cyan('npx @nightwatch/mobile-helper android connect')} to connect to a device.\n`);
30+
31+
return true;
32+
}
33+
34+
if (options.deviceId) {
35+
// If device id is passed then check if the id is valid. If not then prompt user to select a device.
36+
const deviceConnected = devices.find(device => device.udid === options.deviceId);
37+
if (!deviceConnected) {
38+
Logger.log(colors.yellow(`No connected device found with deviceId '${options.deviceId}'.\n`));
39+
40+
options.deviceId = '';
41+
}
42+
}
43+
44+
if (!options.deviceId) {
45+
// if device id not found, or invalid device id is found, then prompt the user
46+
// to select a device from the list of running devices.
47+
const deviceAnswer = await inquirer.prompt({
48+
type: 'list',
49+
name: 'device',
50+
message: 'Select the device to install the APK:',
51+
choices: devices.map(device => device.udid)
52+
});
53+
options.deviceId = deviceAnswer.device;
54+
}
55+
56+
if (!options.path) {
57+
// if path to APK is not provided, then prompt the user to enter the path.
58+
const apkPathAnswer = await inquirer.prompt({
59+
type: 'input',
60+
name: 'apkPath',
61+
message: 'Enter the path to the APK file:'
62+
});
63+
options.path = apkPathAnswer.apkPath;
64+
}
65+
66+
Logger.log();
67+
68+
options.path = path.resolve(process.cwd(), options.path as string);
69+
if (!existsSync(options.path)) {
70+
Logger.log(`${colors.red('No APK file found at: ' + options.path)}\nPlease provide a valid path to the APK file.\n`);
71+
72+
return false;
73+
}
74+
75+
Logger.log('Installing APK...');
76+
77+
const installationStatus = await execBinaryAsync(adbLocation, 'adb', platform, `-s ${options.deviceId} install ${options.path}`);
78+
if (installationStatus?.includes('Success')) {
79+
Logger.log(colors.green('APK installed successfully!\n'));
80+
81+
return true;
82+
}
83+
84+
handleError(installationStatus);
85+
86+
return false;
87+
} catch (err) {
88+
handleError(err);
89+
90+
return false;
91+
}
92+
}
93+
94+
const handleError = (consoleOutput: any) => {
95+
Logger.log(colors.red('\nError while installing APK:'));
96+
97+
let errorMessage = consoleOutput;
98+
if (consoleOutput.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
99+
errorMessage = 'APK with the same package name already exists on the device.\n';
100+
errorMessage += colors.reset(`\nPlease uninstall the app first from the device and then install again.\n`);
101+
errorMessage += colors.reset(`To uninstall, use: ${colors.cyan('npx @nightwatch/mobile-helper android uninstall --app')}\n`);
102+
} else if (consoleOutput.includes('INSTALL_FAILED_OLDER_SDK')) {
103+
errorMessage = 'Target installation location (AVD/Real device) has older SDK version than the minimum requirement of the APK.\n';
104+
}
105+
106+
Logger.log(colors.red(errorMessage));
107+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Options, Platform} from '../../interfaces';
2+
import {installApp} from './app';
3+
4+
export async function install(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
5+
if (options.app) {
6+
return await installApp(options, sdkRoot, platform);
7+
}
8+
9+
return false;
10+
}
11+

src/commands/android/utils/sdk.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import colors from 'ansi-colors';
2+
import {exec, execSync} from 'child_process';
23
import fs from 'fs';
3-
import path from 'path';
44
import {homedir} from 'os';
5-
import {execSync} from 'child_process';
5+
import path from 'path';
66

77
import {copySync, rmDirSync, symbols} from '../../../utils';
8-
import {downloadWithProgressBar, getBinaryNameForOS} from './common';
9-
import {Platform} from '../interfaces';
108
import DOWNLOADS from '../downloads.json';
9+
import {Platform} from '../interfaces';
10+
import {downloadWithProgressBar, getBinaryNameForOS} from './common';
1111

1212

1313
export const getDefaultAndroidSdkRoot = (platform: Platform) => {
@@ -187,6 +187,43 @@ export const execBinarySync = (
187187
}
188188
};
189189

190+
export const execBinaryAsync = (
191+
binaryLocation: string,
192+
binaryName: string,
193+
platform: Platform,
194+
args: string
195+
): Promise<string> => {
196+
return new Promise((resolve, reject) => {
197+
let cmd: string;
198+
if (binaryLocation === 'PATH') {
199+
const binaryFullName = getBinaryNameForOS(platform, binaryName);
200+
cmd = `${binaryFullName} ${args}`;
201+
} else {
202+
const binaryFullName = path.basename(binaryLocation);
203+
const binaryDirPath = path.dirname(binaryLocation);
204+
205+
if (platform === 'windows') {
206+
cmd = `${binaryFullName} ${args}`;
207+
} else {
208+
cmd = `./${binaryFullName} ${args}`;
209+
}
210+
211+
cmd = `cd ${binaryDirPath} && ${cmd}`;
212+
}
213+
214+
exec(cmd, (error, stdout, stderr) => {
215+
if (error) {
216+
console.log(
217+
` ${colors.red(symbols().fail)} Failed to run ${colors.cyan(cmd)}`
218+
);
219+
reject(stderr);
220+
} else {
221+
resolve(stdout.toString());
222+
}
223+
});
224+
});
225+
};
226+
190227
export const getBuildToolsAvailableVersions = (buildToolsPath: string): string[] => {
191228
if (!fs.existsSync(buildToolsPath)) {
192229
return [];

0 commit comments

Comments
 (0)