Files
flipper/desktop/app/src/devices/AndroidDevice.tsx
Pascal Hartig d19eeb2a53 Demote android emulator "errors"
Summary:
Really not errors at all. I'm getting warnings here about my Vulkan
support not working. That shouldn't be logged as a Flipper error.

Reviewed By: mweststrate

Differential Revision: D27260607

fbshipit-source-id: 6f9d2ab0da3fe37bb5c8c185ad627ef0d581e5d5
2021-03-23 09:52:44 -07:00

262 lines
7.4 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import BaseDevice from './BaseDevice';
import adb, {Client as ADBClient} from 'adbkit';
import {Priority, Reader} from 'adbkit-logcat';
import {createWriteStream} from 'fs';
import type {LogLevel, DeviceType} from 'flipper-plugin';
import which from 'which';
import {spawn} from 'child_process';
import {dirname} from 'path';
import {DeviceSpec} from 'flipper-plugin-lib';
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
export default class AndroidDevice extends BaseDevice {
adb: ADBClient;
abiList: Array<string> = [];
sdkVersion: string | undefined = undefined;
pidAppMapping: {[key: number]: string} = {};
private recordingProcess?: Promise<string>;
reader?: Reader;
constructor(
serial: string,
deviceType: DeviceType,
title: string,
adb: ADBClient,
abiList: Array<string>,
sdkVersion: string,
specs: DeviceSpec[] = [],
) {
super(serial, deviceType, title, 'Android', specs);
this.adb = adb;
this.icon = 'mobile';
this.abiList = abiList;
this.sdkVersion = sdkVersion;
}
startLogging() {
this.adb.openLogcat(this.serial, {clear: true}).then((reader) => {
this.reader = reader;
reader.on('entry', (entry) => {
let type: LogLevel = 'unknown';
if (entry.priority === Priority.VERBOSE) {
type = 'verbose';
}
if (entry.priority === Priority.DEBUG) {
type = 'debug';
}
if (entry.priority === Priority.INFO) {
type = 'info';
}
if (entry.priority === Priority.WARN) {
type = 'warn';
}
if (entry.priority === Priority.ERROR) {
type = 'error';
}
if (entry.priority === Priority.FATAL) {
type = 'fatal';
}
this.addLogEntry({
tag: entry.tag,
pid: entry.pid,
tid: entry.tid,
message: entry.message,
date: entry.date,
type,
});
});
});
}
stopLogging() {
this.reader?.end();
}
reverse(ports: [number, number]): Promise<void> {
return Promise.all(
ports.map((port) =>
this.adb.reverse(this.serial, `tcp:${port}`, `tcp:${port}`),
),
).then(() => {
return;
});
}
clearLogs(): Promise<void> {
return this.executeShell(['logcat', '-c']);
}
navigateToLocation(location: string) {
const shellCommand = `am start ${encodeURI(location)}`;
this.adb.shell(this.serial, shellCommand);
}
async screenshot(): Promise<Buffer> {
if (this.isArchived) {
return Buffer.from([]);
}
return new Promise((resolve, reject) => {
this.adb.screencap(this.serial).then((stream) => {
const chunks: Array<Buffer> = [];
stream
.on('data', (chunk: Buffer) => chunks.push(chunk))
.once('end', () => {
resolve(Buffer.concat(chunks));
})
.once('error', reject);
});
});
}
async screenCaptureAvailable(): Promise<boolean> {
if (this.isArchived) {
return false;
}
try {
await this.executeShell(
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,
);
return true;
} catch (_e) {
return false;
}
}
private async executeShell(command: string | string[]): Promise<void> {
const output = await this.adb
.shell(this.serial, command)
.then(adb.util.readAll)
.then((output: Buffer) => output.toString().trim());
if (output) {
throw new Error(output);
}
}
private async getSdkVersion(): Promise<number> {
return await this.adb
.shell(this.serial, 'getprop ro.build.version.sdk')
.then(adb.util.readAll)
.then((output) => Number(output.toString().trim()));
}
private async isValidFile(filePath: string): Promise<boolean> {
const sdkVersion = await this.getSdkVersion();
const fileSize = await this.adb
.shell(this.serial, `ls -l "${filePath}"`)
.then(adb.util.readAll)
.then((output: Buffer) => output.toString().trim().split(' '))
.then((x) => x.filter(Boolean))
.then((x) => (sdkVersion > 23 ? Number(x[4]) : Number(x[3])));
return fileSize > 0;
}
async startScreenCapture(destination: string) {
await this.executeShell(
`mkdir -p "${DEVICE_RECORDING_DIR}" && echo -n > "${DEVICE_RECORDING_DIR}/.nomedia"`,
);
const recordingLocation = `${DEVICE_RECORDING_DIR}/video.mp4`;
let newSize: string | undefined;
try {
const sizeString = (
await adb.util.readAll(await this.adb.shell(this.serial, 'wm size'))
).toString();
const size = sizeString.split(' ').slice(-1).pop()?.split('x');
if (size && size.length === 2) {
const width = parseInt(size[0], 10);
const height = parseInt(size[1], 10);
if (width > height) {
newSize = '1280x720';
} else {
newSize = '720x1280';
}
}
} catch (err) {
console.error('Error while getting device size', err);
}
const sizeArg = newSize ? `--size ${newSize}` : '';
const cmd = `screenrecord ${sizeArg} "${recordingLocation}"`;
this.recordingProcess = this.adb
.shell(this.serial, cmd)
.then(adb.util.readAll)
.then(async (output) => {
const isValid = await this.isValidFile(recordingLocation);
if (!isValid) {
const outputMessage = output.toString().trim();
throw new Error(
'Recording was not properly started: \n' + outputMessage,
);
}
})
.then(
(_) =>
new Promise((resolve, reject) => {
this.adb.pull(this.serial, recordingLocation).then((stream) => {
stream.on('end', resolve as () => void);
stream.on('error', reject);
stream.pipe(createWriteStream(destination, {autoClose: true}), {
end: true,
});
});
}),
)
.then((_) => destination);
return this.recordingProcess.then((_) => {});
}
async stopScreenCapture(): Promise<string> {
const {recordingProcess} = this;
if (!recordingProcess) {
return Promise.reject(new Error('Recording was not properly started'));
}
await this.adb.shell(this.serial, `pkill -l2 screenrecord`);
const destination = await recordingProcess;
this.recordingProcess = undefined;
return destination;
}
disconnect() {
if (this.recordingProcess) {
this.stopScreenCapture();
}
super.disconnect();
}
}
export async function launchEmulator(name: string, coldBoot: boolean = false) {
// On Linux, you must run the emulator from the directory it's in because
// reasons ...
return which('emulator')
.then((emulatorPath) => {
if (emulatorPath) {
const child = spawn(
emulatorPath,
[`@${name}`, ...(coldBoot ? ['-no-snapshot-load'] : [])],
{
detached: true,
cwd: dirname(emulatorPath),
},
);
child.stderr.on('data', (data) => {
console.warn(`Android emulator stderr: ${data}`);
});
child.on('error', (e) => console.warn('Android emulator error:', e));
} else {
throw new Error('Could not get emulator path');
}
})
.catch((e) => console.error('Android emulator startup failed:', e));
}