Set device features on device initialization

Summary:
1. Identify if device supports screenshots/screen recording when it is created.
2. Disable screen recording/screenshot buttons when they are not supported

Reviewed By: passy

Differential Revision: D34611133

fbshipit-source-id: 82ad2d67e4af482d9becf7995187667b5d99bc36
This commit is contained in:
Andrey Goncharov
2022-03-04 02:00:23 -08:00
committed by Facebook GitHub Bot
parent 04e7c7282b
commit 6c74f2dd18
19 changed files with 81 additions and 75 deletions

View File

@@ -43,6 +43,10 @@ export type DeviceDescription = {
readonly deviceType: DeviceType;
readonly serial: string;
readonly icon?: string;
readonly features: {
screenshotAvailable: boolean;
screenCaptureAvailable: boolean;
};
// Android specific information
readonly specs?: DeviceSpec[];
readonly abiList?: string[];
@@ -205,8 +209,6 @@ export type FlipperServerCommands = {
'get-config': () => Promise<FlipperServerConfig>;
'get-changelog': () => Promise<string>;
'device-list': () => Promise<DeviceDescription[]>;
'device-supports-screenshot': (serial: string) => Promise<boolean>;
'device-supports-screencapture': (serial: string) => Promise<boolean>;
'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
'device-start-screencapture': (
serial: string,

View File

@@ -41,7 +41,7 @@ export interface Device {
clearLogs(): Promise<void>;
sendMetroCommand(command: string): Promise<void>;
navigateToLocation(location: string): Promise<void>;
screenshot(): Promise<Uint8Array>;
screenshot(): Promise<Uint8Array | undefined>;
}
export type DevicePluginPredicate = (device: Device) => boolean;

View File

@@ -333,14 +333,6 @@ export class FlipperServerImpl implements FlipperServer {
'device-list': async () => {
return Array.from(this.devices.values()).map((d) => d.info);
},
'device-supports-screenshot': async (serial: string) =>
this.devices.has(serial)
? this.getDevice(serial).screenshotAvailable()
: false,
'device-supports-screencapture': async (serial: string) =>
this.devices.has(serial)
? this.getDevice(serial).screenCaptureAvailable()
: false,
'device-take-screenshot': async (serial: string) =>
Base64.fromUint8Array(await this.getDevice(serial).screenshot()),
'device-start-screencapture': async (serial, destination) =>

View File

@@ -26,6 +26,10 @@ export default class DummyDevice extends ServerDevice {
deviceType: 'dummy',
title,
os,
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
}
}

View File

@@ -44,25 +44,19 @@ export abstract class ServerDevice {
*/
disconnect(): void {
this.connected = false;
this.info.features.screenCaptureAvailable = false;
this.info.features.screenshotAvailable = false;
this.logListener.stop();
this.crashWatcher.stop();
this.flipperServer.pluginManager.stopAllServerAddOns(this.info.serial);
}
async screenshotAvailable(): Promise<boolean> {
return false;
}
screenshot(): Promise<Buffer> {
return Promise.reject(
new Error('No screenshot support for current device'),
);
}
async screenCaptureAvailable(): Promise<boolean> {
return false;
}
async startScreenCapture(_destination: string): Promise<void> {
throw new Error('startScreenCapture not implemented on BaseDevice ');
}

View File

@@ -49,6 +49,10 @@ export default class AndroidDevice extends ServerDevice {
specs,
abiList,
sdkVersion,
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
this.adb = adb;
@@ -115,7 +119,7 @@ export default class AndroidDevice extends ServerDevice {
});
}
async screenCaptureAvailable(): Promise<boolean> {
async screenRecordAvailable(): Promise<boolean> {
try {
await this.executeShellOrDie(
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,

View File

@@ -26,10 +26,4 @@ export default class KaiOSDevice extends AndroidDevice {
'KaiOS',
]);
}
async screenCaptureAvailable() {
// The default way of capturing screenshots through adb does not seem to work
// There is a way of getting a screenshot through KaiOS dev tools though
return false;
}
}

View File

@@ -86,6 +86,18 @@ export class AndroidDeviceManager {
);
});
}
// The default way of capturing screenshots through adb does not seem to work
// There is a way of getting a screenshot through KaiOS dev tools though
if (androidLikeDevice instanceof AndroidDevice) {
const screenRecordAvailable =
await androidLikeDevice.screenRecordAvailable();
androidLikeDevice.info.features.screenCaptureAvailable =
screenRecordAvailable;
androidLikeDevice.info.features.screenshotAvailable =
screenRecordAvailable;
}
resolve(androidLikeDevice);
} catch (e) {
reject(e);

View File

@@ -18,6 +18,10 @@ export default class MacDevice extends ServerDevice {
title: 'Mac',
os: 'MacOS',
icon: 'app-apple',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
}
}

View File

@@ -18,6 +18,10 @@ export default class WindowsDevice extends ServerDevice {
title: 'Windows',
os: 'Windows',
icon: 'app-microsoft-windows',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
}
}

View File

@@ -36,6 +36,10 @@ export default class IOSDevice extends ServerDevice {
title,
os: 'iOS',
icon: 'mobile',
features: {
screenCaptureAvailable: true,
screenshotAvailable: true,
},
});
this.buffer = '';
this.iOSBridge = iOSBridge;
@@ -76,10 +80,6 @@ export default class IOSDevice extends ServerDevice {
});
}
async screenCaptureAvailable() {
return this.connected;
}
async startScreenCapture(destination: string) {
const recording = this.recording;
if (recording) {

View File

@@ -64,6 +64,10 @@ export default class MetroDevice extends ServerDevice {
title: 'React Native',
os: 'Metro',
icon: 'mobile',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
if (ws) {
this.ws = ws;

View File

@@ -8,7 +8,7 @@
*/
import {Button as AntButton, message} from 'antd';
import React, {useState, useEffect, useCallback} from 'react';
import React, {useState, useCallback} from 'react';
import {capture, getCaptureLocation, getFileName} from '../utils/screenshot';
import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons';
import {useStore} from '../utils/useStore';
@@ -22,21 +22,8 @@ async function openFile(path: string) {
export default function ScreenCaptureButtons() {
const selectedDevice = useStore((state) => state.connections.selectedDevice);
const [isTakingScreenshot, setIsTakingScreenshot] = useState(false);
const [isRecordingAvailable, setIsRecordingAvailable] = useState(false);
const [isRecording, setIsRecording] = useState(false);
useEffect(() => {
let canceled = false;
selectedDevice?.screenCaptureAvailable().then((result) => {
if (!canceled) {
setIsRecordingAvailable(result);
}
});
return () => {
canceled = true;
};
}, [selectedDevice]);
const handleScreenshot = useCallback(() => {
setIsTakingScreenshot(true);
return capture(selectedDevice!)
@@ -87,7 +74,10 @@ export default function ScreenCaptureButtons() {
title="Take Screenshot"
type="ghost"
onClick={handleScreenshot}
disabled={!selectedDevice}
disabled={
!selectedDevice ||
!selectedDevice.description.features.screenshotAvailable
}
loading={isTakingScreenshot}
/>
<AntButton
@@ -95,7 +85,10 @@ export default function ScreenCaptureButtons() {
title="Make Screen Recording"
type={isRecording ? 'primary' : 'ghost'}
onClick={handleRecording}
disabled={!selectedDevice || !isRecordingAvailable}
disabled={
!selectedDevice ||
!selectedDevice.description.features.screenCaptureAvailable
}
danger={isRecording}
/>
</>

View File

@@ -25,14 +25,12 @@ type DispatchFromProps = {};
type State = {
recording: boolean;
recordingEnabled: boolean;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
export default class VideoRecordingButton extends Component<Props, State> {
state: State = {
recording: false,
recordingEnabled: true,
};
startRecording = async () => {
@@ -80,7 +78,6 @@ export default class VideoRecordingButton extends Component<Props, State> {
}
};
render() {
const {recordingEnabled} = this.state;
const {selectedDevice} = this.props;
return (
<Button
@@ -89,7 +86,10 @@ export default class VideoRecordingButton extends Component<Props, State> {
pulse={this.state.recording}
selected={this.state.recording}
title="Make Screen Recording"
disabled={!selectedDevice || !recordingEnabled}
disabled={
!selectedDevice ||
!selectedDevice.description.features.screenCaptureAvailable
}
type={this.state.recording ? 'danger' : 'primary'}>
<Glyph
name={this.state.recording ? 'stop-playback' : 'camcorder'}

View File

@@ -45,6 +45,10 @@ export default class ArchivedDevice extends BaseDevice {
os: options.os,
serial: options.serial,
icon: 'box',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
},
);
this.connected.set(false);

View File

@@ -235,32 +235,15 @@ export default class BaseDevice implements Device {
return this.flipperServer.exec('device-navigate', this.serial, location);
}
async screenshotAvailable(): Promise<boolean> {
if (this.isArchived) {
return false;
}
return this.flipperServer.exec('device-supports-screenshot', this.serial);
}
async screenshot(): Promise<Uint8Array> {
if (this.isArchived) {
return new Uint8Array();
async screenshot(): Promise<Uint8Array | undefined> {
if (!this.description.features.screenshotAvailable || this.isArchived) {
return;
}
return Base64.toUint8Array(
await this.flipperServer.exec('device-take-screenshot', this.serial),
);
}
async screenCaptureAvailable(): Promise<boolean> {
if (this.isArchived) {
return false;
}
return this.flipperServer.exec(
'device-supports-screencapture',
this.serial,
);
}
async startScreenCapture(destination: string): Promise<void> {
return this.flipperServer.exec(
'device-start-screencapture',

View File

@@ -26,6 +26,10 @@ export class TestDevice extends BaseDevice {
title,
os,
specs,
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
});
}

View File

@@ -11,6 +11,7 @@ import BaseDevice from '../devices/BaseDevice';
import {reportPlatformFailures} from 'flipper-common';
import {getRenderHostInstance} from '../RenderHost';
import {getFlipperLib, path} from 'flipper-plugin';
import {assertNotNull} from './assertNotNull';
export function getCaptureLocation() {
return (
@@ -27,7 +28,7 @@ export function getFileName(extension: 'png' | 'mp4'): string {
export async function capture(device: BaseDevice): Promise<string> {
if (!device.connected.get()) {
console.log('Skipping screenshot for disconnected device');
console.info('Skipping screenshot for disconnected device');
return '';
}
const pngPath = path.join(getCaptureLocation(), getFileName('png'));
@@ -36,9 +37,16 @@ export async function capture(device: BaseDevice): Promise<string> {
// again to write in a file, probably easier to change screenshot api to `device.screenshot(): path`
device
.screenshot()
.then((buffer) =>
getFlipperLib().remoteServerContext.fs.writeFileBinary(pngPath, buffer),
)
.then((buffer) => {
assertNotNull(
buffer,
`Device ${device.description.deviceType}:${device.description.os} does not support taking screenshots`,
);
return getFlipperLib().remoteServerContext.fs.writeFileBinary(
pngPath,
buffer,
);
})
.then(() => pngPath),
'captureScreenshot',
);

View File

@@ -73,7 +73,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
});
const screenshot = await client.device.screenshot();
if (screenshot.byteLength === 0) {
if (!screenshot) {
console.warn(
'[navigation] Could not retrieve valid screenshot from the device.',
);