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 1 commit
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
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
111 changes: 111 additions & 0 deletions src/commands/android/subcommands/install/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import colors from 'ansi-colors';
import ADB from 'appium-adb';
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved
import {existsSync} from 'fs';
import inquirer from 'inquirer';
import {homedir} from 'os';
import path from 'path';

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

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`);
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`);

return false;
}
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved

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;
} else if (devices.length === 1) {
// if only one device is connected, then set that device's id to options.deviceId
options.deviceId = devices[0].udid;
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved
}

if (options.deviceId && devices.length > 1) {
// If device id is passed and there are multiple devices connected 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('Invalid device Id passed!')}\n`);
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved

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();
}

itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved

options.path = path.resolve(homedir(), options.path as string);
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved
if (!existsSync(options.path)) {
Logger.log(`${colors.red('APK file not found!')} Please 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('Error occured 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
} 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(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 | null> => {
itsspriyansh marked this conversation as resolved.
Show resolved Hide resolved
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