Skip to content
Open
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
129 changes: 51 additions & 78 deletions lib/android/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { promisify } from 'node:util';
import { mkdir as _mkdir, createWriteStream, createReadStream } from 'node:fs';
import path from 'node:path';
import { EOL as endOfLine } from 'node:os';
import { execa } from 'execa';
import { getLogger } from '@sitespeed.io/log';
import pkg from '@devicefarmer/adbkit';
const { Adb } = pkg;
import { pathToFolder } from '../support/pathToFolder.js';
import { loadUsbPowerProfiler } from '../support/usbPower.js';
import { getProperty } from '../support/util.js';
const log = getLogger('browsertime.android');
const mkdir = promisify(_mkdir);
const delay = ms => new Promise(res => setTimeout(res, ms));

export class Android {
Expand All @@ -23,7 +18,6 @@ export class Android {
}

Android.instance = this;
this.client = Adb.createClient();

this.id = getProperty(
options,
Expand All @@ -42,44 +36,53 @@ export class Android {

async _init() {
if (!this.id) {
const devices = await this.client.listDevices();
const { stdout } = await execa('adb', ['devices']);
// just take the first phone online
if (devices.length > 0) {
this.id = devices[0].id;
const device = stdout
.split(/\r?\n/)
.map(line => line.trim().split(/\s+/))
.find(parts => parts[1] === 'device');
if (device) {
this.id = device[0];
} else {
throw new Error('No Android phone was found');
}
}

this.device = this.client.getDevice(this.id);

if (!this.sdcard) {
this.sdcard = await this._runCommandAndGet('echo $EXTERNAL_STORAGE');
}
}

_adb(arguments_, options = {}) {
return execa('adb', ['-s', this.id, ...arguments_], options);
}

async _runCommand(command) {
return this.device.shell(command);
const result = await this._adb(['shell', command], {
all: true,
reject: false
});
// A missing exit code means adb itself could not run
if (result.exitCode === undefined) {
throw result;
}
return result.all;
}

async _runCommandAndGet(command) {
const data = await this.device.shell(command);
const output = await Adb.util.readAll(data);
return output.toString().trim();
const output = await this._runCommand(command);
return output.trim();
}

async _runAsRootAndGet(command) {
const data = await this.device.shell('su - root -c "' + command + '"');
const output = await Adb.util.readAll(data);
return output.toString().trim();
return this._runCommandAndGet('su - root -c "' + command + '"');
}

async _runAsRoot(command) {
const data = await this.device.shell(
const result = await this._runCommandAndGet(
'su - root -c "' + command + ' && echo SUCCESS || echo FAIL"'
);
const output = await Adb.util.readAll(data);
const result = output.toString().trim();
if (result === 'FAIL') {
log.error('Failing running as root:' + command);
}
Expand All @@ -88,37 +91,18 @@ export class Android {

async _downloadFile(sourcePath, destinationPath) {
log.debug(`Pulling to ${destinationPath} from ${sourcePath}`);

const transfer = await this.device.pull(sourcePath);

return new Promise((resolve, reject) => {
transfer.on('end', function () {
resolve();
});
transfer.on('error', reject);
transfer.pipe(createWriteStream(destinationPath));
});
await this._adb(['pull', sourcePath, destinationPath]);
}

async _uploadFile(sourcePath, destinationPath) {
log.debug(`Pushing to ${destinationPath} from ${sourcePath}`);
return this.device.push(createReadStream(sourcePath), destinationPath);
await this._adb(['push', sourcePath, destinationPath]);
}

async _downloadDir(sourcePath, destinationPath) {
const files = await this.device.readdir(sourcePath);

for (const file of files) {
const fullSourcePath = `${sourcePath}/${file.name}`;
const fullDestinationPath = path.join(destinationPath, file.name);

if (file.isFile()) {
await this._downloadFile(fullSourcePath, fullDestinationPath);
} else if (file.isDirectory()) {
await mkdir(fullDestinationPath, { recursive: true });
await this._downloadDir(fullSourcePath, fullDestinationPath);
}
}
// The trailing /. makes adb copy the content of the directory
// instead of the directory itself
await this._adb(['pull', `${sourcePath}/.`, destinationPath]);
}

getFullPathOnSdCard(path) {
Expand Down Expand Up @@ -147,7 +131,7 @@ export class Android {
}

async reboot() {
await this.device.reboot(this.id);
await this._adb(['reboot']);
return delay(60_000);
}

Expand All @@ -161,25 +145,17 @@ export class Android {
}

async getMeta() {
const rawModel = await Adb.util.readAll(
await this._runCommand(`getprop ro.product.model`)
);
const rawModel = await this._runCommand(`getprop ro.product.model`);
const model = rawModel.toString().trim();
const rawName = await Adb.util.readAll(
await this._runCommand(`getprop ro.product.name`)
);
const rawName = await this._runCommand(`getprop ro.product.name`);
const name = rawName.toString().trim();
const rawDevice = await Adb.util.readAll(
await this._runCommand(`getprop ro.product.device`)
);
const rawDevice = await this._runCommand(`getprop ro.product.device`);
const device = rawDevice.toString().trim();
const rawId = await Adb.util.readAll(
await this._runCommand(`getprop ro.serialno`)
);
const rawId = await this._runCommand(`getprop ro.serialno`);
const id = rawId.toString().trim();
const wifi = await this.getWifi();
const rawRelease = await Adb.util.readAll(
await this._runCommand(`getprop ro.build.version.release `)
const rawRelease = await this._runCommand(
`getprop ro.build.version.release `
);
const androidVersion = rawRelease.toString().trim();
return { model, name, device, androidVersion, id, wifi };
Expand All @@ -193,10 +169,11 @@ export class Android {
}

async addDevtoolsFw() {
return this.device.forward(
return this._adb([
'forward',
'tcp:' + this.port,
'localabstract:chrome_devtools_remote'
);
]);
}

async removeDevtoolsFw() {
Expand Down Expand Up @@ -225,8 +202,13 @@ export class Android {
}

async startVideo() {
return this._runCommand(
`screenrecord --bit-rate 8000000 ${this.sdcard}/browsertime.mp4`
// screenrecord runs until stopVideo kills it, so do not wait for it
this._adb(
[
'shell',
`screenrecord --bit-rate 8000000 ${this.sdcard}/browsertime.mp4`
],
{ reject: false }
);
}

Expand All @@ -241,10 +223,8 @@ export class Android {
}

async getWifi() {
const rawWifiInfo = await Adb.util.readAll(
await this._runCommand(
`dumpsys netstats | grep -E 'iface=wlan.*networkId'`
)
const rawWifiInfo = await this._runCommand(
`dumpsys netstats | grep -E 'iface=wlan.*networkId'`
);
const wifiInfo = rawWifiInfo.toString().trim();
const wifi = wifiInfo.match(/"[^"]*"|^[^"]*$/)[0].replaceAll('"', '');
Expand Down Expand Up @@ -303,9 +283,7 @@ export class Android {
// This method is expected to work on the Moto G5 and the Pixel 2.
const cmd1 = `pidof ${packageName}`;
log.debug(`pidof ${cmd1}`);
const proc1 = await this._runCommand(cmd1);
const ps1 = await Adb.util.readAll(proc1);
const ps = ps1.toString();
const ps = await this._runCommand(cmd1);
log.debug(`pidof ${ps}`);

if (
Expand All @@ -332,9 +310,7 @@ export class Android {
// This method was tested on the Galaxy S5.
const cmd = `ps`;
log.debug(`pidof ${cmd}`);
const proc = await this._runCommand(cmd);
const psTemporary = await Adb.util.readAll(proc);
const ps = psTemporary.toString();
const ps = await this._runCommand(cmd);
log.debug(`pidof ${ps}`);

const lines = ps.split(/[\n\r]+/);
Expand Down Expand Up @@ -387,10 +363,7 @@ export class Android {
// `cut` and `awk`, to make this more robust.
const cut = `echo $EPOCHREALTIME && cut -d ' ' -f 1 /proc/uptime && cut -d ' ' -f 22 /proc/self/stat /proc/${pid}/stat`;
// const awk = `date +%s%3N && awk '{print $1}' /proc/uptime && awk '{print $22}' /proc/self/stat /proc/${pid}/stat`;
const statAndTimestamp = await Adb.util.readAll(
await this._runCommand(cut)
);
const output = statAndTimestamp.toString();
const output = await this._runCommand(cut);

const [
dateInS,
Expand Down
Loading
Loading