Summary: No device was using this feature, so doesn't look like there is a reason to keep it. ...if somebody was about to use this feature, let me know :). But probably, if we need this, a much more simple solution is to determine the visible columns with a simple switch on device type in the logs plugin, even though that is strictly speaking less scalable. But the current solution feels a bit over-engineered for something that is not really used. Marked [interesting] as I might be missing some background concept which would make this relevant as well. Reviewed By: jknoxville, nikoant Differential Revision: D22763507 fbshipit-source-id: ecdbc779cbbfa3b0b72c80b459b12c1a25bf3fc4
192 lines
5.3 KiB
TypeScript
192 lines
5.3 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} from 'adbkit-logcat';
|
|
import ArchivedDevice from './ArchivedDevice';
|
|
import {createWriteStream} from 'fs';
|
|
import {LogLevel, DeviceType} from 'flipper-plugin';
|
|
|
|
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
|
|
|
export default class AndroidDevice extends BaseDevice {
|
|
constructor(
|
|
serial: string,
|
|
deviceType: DeviceType,
|
|
title: string,
|
|
adb: ADBClient,
|
|
abiList: Array<string>,
|
|
sdkVersion: string,
|
|
) {
|
|
super(serial, deviceType, title, 'Android');
|
|
this.adb = adb;
|
|
this.icon = 'icons/android.svg';
|
|
this.abiList = abiList;
|
|
this.sdkVersion = sdkVersion;
|
|
this.adb.openLogcat(this.serial).then((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,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
adb: ADBClient;
|
|
abiList: Array<string> = [];
|
|
sdkVersion: string | undefined = undefined;
|
|
pidAppMapping: {[key: number]: string} = {};
|
|
private recordingProcess?: Promise<string>;
|
|
|
|
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> {
|
|
this.logEntries = [];
|
|
return this.executeShell(['logcat', '-c']);
|
|
}
|
|
|
|
archive(): ArchivedDevice {
|
|
return new ArchivedDevice({
|
|
serial: this.serial,
|
|
deviceType: this.deviceType,
|
|
title: this.title,
|
|
os: this.os,
|
|
logEntries: [...this.logEntries],
|
|
screenshotHandle: null,
|
|
});
|
|
}
|
|
|
|
navigateToLocation(location: string) {
|
|
const shellCommand = `am start ${encodeURI(location)}`;
|
|
this.adb.shell(this.serial, shellCommand);
|
|
}
|
|
|
|
screenshot(): Promise<Buffer> {
|
|
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> {
|
|
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 isValidFile(filePath: string): Promise<boolean> {
|
|
const fileSize = await this.adb
|
|
.shell(this.serial, `ls -l "${filePath}"`)
|
|
.then(adb.util.readAll)
|
|
.then((output: Buffer) => output.toString().trim().split(' '))
|
|
.then((x) => Number(x[4]));
|
|
|
|
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`;
|
|
this.recordingProcess = this.adb
|
|
.shell(this.serial, `screenrecord --bugreport "${recordingLocation}"`)
|
|
.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);
|
|
stream.on('error', reject);
|
|
stream.pipe(createWriteStream(destination));
|
|
});
|
|
}),
|
|
)
|
|
.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, `pgrep 'screenrecord' -L 2`);
|
|
const destination = await recordingProcess;
|
|
this.recordingProcess = undefined;
|
|
return destination;
|
|
}
|
|
}
|