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

feat: install apk #56

Merged
merged 3 commits into from
Aug 14, 2024
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
5 changes: 5 additions & 0 deletions src/commands/android/subcommands/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,8 @@ export async function showConnectedEmulators() {
return false;
}
}

export function showMissingRequirementsHelp() {
Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android --standalone')} to setup missing requirements.`);
Logger.log(`(Remove the ${colors.gray('--standalone')} flag from the above command if setting up for testing.)\n`);
}
9 changes: 6 additions & 3 deletions src/commands/android/subcommands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import colors from 'ansi-colors';
import * as dotenv from 'dotenv';
import path from 'path';

import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common';
import {connect} from './connect';
import {getPlatformName} from '../../../utils';
import Logger from '../../../logger';
import {getPlatformName} from '../../../utils';
import {Options, Platform} from '../interfaces';
import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common';
import {connect} from './connect';
import {install} from './install';

export class AndroidSubcommand {
sdkRoot: string;
Expand Down Expand Up @@ -56,6 +57,8 @@ export class AndroidSubcommand {
async executeSubcommand(): Promise<boolean> {
if (this.subcommand === 'connect') {
return await connect(this.options, this.sdkRoot, this.platform);
} else if (this.subcommand === 'install') {
return await install(this.options, this.sdkRoot, this.platform);
}

return false;
Expand Down
107 changes: 107 additions & 0 deletions src/commands/android/subcommands/install/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import colors from 'ansi-colors';
import {existsSync} from 'fs';
import inquirer from 'inquirer';
import path from 'path';

import Logger from '../../../../logger';
import {symbols} from '../../../../utils';
import {Options, Platform} from '../../interfaces';
import ADB from '../../utils/appium-adb';
import {getBinaryLocation} from '../../utils/common';
import {execBinaryAsync} from '../../utils/sdk';
import {showMissingRequirementsHelp} from '../common';

export async function installApp(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
try {
const adbLocation = getBinaryLocation(sdkRoot, platform, 'adb', true);
if (!adbLocation) {
Logger.log(` ${colors.red(symbols().fail)} ${colors.cyan('adb')} binary not found.\n`);
showMissingRequirementsHelp();

return false;
}

const adb = await ADB.createADB({allowOfflineDevices: true});
const devices = await adb.getConnectedDevices();

if (!devices.length) {
Logger.log(`${colors.red('No device found running.')} Please connect a device to install the APK.`);
Logger.log(`Use ${colors.cyan('npx @nightwatch/mobile-helper android connect')} to connect to a device.\n`);

return true;
}

if (options.deviceId) {
// If device id is passed then check if the id is valid. If not then prompt user to select a device.
const deviceConnected = devices.find(device => device.udid === options.deviceId);
if (!deviceConnected) {
Logger.log(colors.yellow(`No connected device found with deviceId '${options.deviceId}'.\n`));

options.deviceId = '';
}
}

if (!options.deviceId) {
// if device id not found, or invalid device id is found, then prompt the user
// to select a device from the list of running devices.
const deviceAnswer = await inquirer.prompt({
type: 'list',
name: 'device',
message: 'Select the device to install the APK:',
choices: devices.map(device => device.udid)
});
options.deviceId = deviceAnswer.device;
}

if (!options.path) {
// if path to APK is not provided, then prompt the user to enter the path.
const apkPathAnswer = await inquirer.prompt({
type: 'input',
name: 'apkPath',
message: 'Enter the path to the APK file:'
});
options.path = apkPathAnswer.apkPath;
}

Logger.log();

options.path = path.resolve(process.cwd(), options.path as string);
if (!existsSync(options.path)) {
Logger.log(`${colors.red('No APK file found at: ' + options.path)}\nPlease provide a valid path to the APK file.\n`);

return false;
}

Logger.log('Installing APK...');

const installationStatus = await execBinaryAsync(adbLocation, 'adb', platform, `-s ${options.deviceId} install ${options.path}`);
if (installationStatus?.includes('Success')) {
Logger.log(colors.green('APK installed successfully!\n'));

return true;
}

handleError(installationStatus);

return false;
} catch (err) {
handleError(err);

return false;
}
}

const handleError = (consoleOutput: any) => {
Logger.log(colors.red('\nError while installing APK:'));

let errorMessage = consoleOutput;
if (consoleOutput.includes('INSTALL_FAILED_ALREADY_EXISTS')) {
errorMessage = 'APK with the same package name already exists on the device.\n';
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved
errorMessage += colors.reset(`\nPlease uninstall the app first from the device and then install again.\n`);
errorMessage += colors.reset(`To uninstall, use: ${colors.cyan('npx @nightwatch/mobile-helper android uninstall --app')}\n`);
} else if (consoleOutput.includes('INSTALL_FAILED_OLDER_SDK')) {
errorMessage = 'Target installation location (AVD/Real device) has older SDK version than the minimum requirement of the APK.\n';
}

Logger.log(colors.red(errorMessage));
};
11 changes: 11 additions & 0 deletions src/commands/android/subcommands/install/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Options, Platform} from '../../interfaces';
import {installApp} from './app';

export async function install(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
if (options.app) {
return await installApp(options, sdkRoot, platform);
}

return false;
}

45 changes: 41 additions & 4 deletions src/commands/android/utils/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import colors from 'ansi-colors';
import {exec, execSync} from 'child_process';
import fs from 'fs';
import path from 'path';
import {homedir} from 'os';
import {execSync} from 'child_process';
import path from 'path';

import {copySync, rmDirSync, symbols} from '../../../utils';
import {downloadWithProgressBar, getBinaryNameForOS} from './common';
import {Platform} from '../interfaces';
import DOWNLOADS from '../downloads.json';
import {Platform} from '../interfaces';
import {downloadWithProgressBar, getBinaryNameForOS} from './common';


export const getDefaultAndroidSdkRoot = (platform: Platform) => {
Expand Down Expand Up @@ -187,6 +187,43 @@ export const execBinarySync = (
}
};

export const execBinaryAsync = (
binaryLocation: string,
binaryName: string,
platform: Platform,
args: string
): Promise<string> => {
return new Promise((resolve, reject) => {
let cmd: string;
if (binaryLocation === 'PATH') {
const binaryFullName = getBinaryNameForOS(platform, binaryName);
cmd = `${binaryFullName} ${args}`;
} else {
const binaryFullName = path.basename(binaryLocation);
const binaryDirPath = path.dirname(binaryLocation);

if (platform === 'windows') {
cmd = `${binaryFullName} ${args}`;
} else {
cmd = `./${binaryFullName} ${args}`;
}

cmd = `cd ${binaryDirPath} && ${cmd}`;
}

exec(cmd, (error, stdout, stderr) => {
if (error) {
console.log(
` ${colors.red(symbols().fail)} Failed to run ${colors.cyan(cmd)}`
);
reject(stderr);
} else {
resolve(stdout.toString());
}
});
});
};

export const getBuildToolsAvailableVersions = (buildToolsPath: string): string[] => {
if (!fs.existsSync(buildToolsPath)) {
return [];
Expand Down
Loading