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:
committed by
Facebook GitHub Bot
parent
04e7c7282b
commit
6c74f2dd18
@@ -43,6 +43,10 @@ export type DeviceDescription = {
|
|||||||
readonly deviceType: DeviceType;
|
readonly deviceType: DeviceType;
|
||||||
readonly serial: string;
|
readonly serial: string;
|
||||||
readonly icon?: string;
|
readonly icon?: string;
|
||||||
|
readonly features: {
|
||||||
|
screenshotAvailable: boolean;
|
||||||
|
screenCaptureAvailable: boolean;
|
||||||
|
};
|
||||||
// Android specific information
|
// Android specific information
|
||||||
readonly specs?: DeviceSpec[];
|
readonly specs?: DeviceSpec[];
|
||||||
readonly abiList?: string[];
|
readonly abiList?: string[];
|
||||||
@@ -205,8 +209,6 @@ export type FlipperServerCommands = {
|
|||||||
'get-config': () => Promise<FlipperServerConfig>;
|
'get-config': () => Promise<FlipperServerConfig>;
|
||||||
'get-changelog': () => Promise<string>;
|
'get-changelog': () => Promise<string>;
|
||||||
'device-list': () => Promise<DeviceDescription[]>;
|
'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-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
|
||||||
'device-start-screencapture': (
|
'device-start-screencapture': (
|
||||||
serial: string,
|
serial: string,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export interface Device {
|
|||||||
clearLogs(): Promise<void>;
|
clearLogs(): Promise<void>;
|
||||||
sendMetroCommand(command: string): Promise<void>;
|
sendMetroCommand(command: string): Promise<void>;
|
||||||
navigateToLocation(location: string): Promise<void>;
|
navigateToLocation(location: string): Promise<void>;
|
||||||
screenshot(): Promise<Uint8Array>;
|
screenshot(): Promise<Uint8Array | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DevicePluginPredicate = (device: Device) => boolean;
|
export type DevicePluginPredicate = (device: Device) => boolean;
|
||||||
|
|||||||
@@ -333,14 +333,6 @@ export class FlipperServerImpl implements FlipperServer {
|
|||||||
'device-list': async () => {
|
'device-list': async () => {
|
||||||
return Array.from(this.devices.values()).map((d) => d.info);
|
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) =>
|
'device-take-screenshot': async (serial: string) =>
|
||||||
Base64.fromUint8Array(await this.getDevice(serial).screenshot()),
|
Base64.fromUint8Array(await this.getDevice(serial).screenshot()),
|
||||||
'device-start-screencapture': async (serial, destination) =>
|
'device-start-screencapture': async (serial, destination) =>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export default class DummyDevice extends ServerDevice {
|
|||||||
deviceType: 'dummy',
|
deviceType: 'dummy',
|
||||||
title,
|
title,
|
||||||
os,
|
os,
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,25 +44,19 @@ export abstract class ServerDevice {
|
|||||||
*/
|
*/
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
|
this.info.features.screenCaptureAvailable = false;
|
||||||
|
this.info.features.screenshotAvailable = false;
|
||||||
this.logListener.stop();
|
this.logListener.stop();
|
||||||
this.crashWatcher.stop();
|
this.crashWatcher.stop();
|
||||||
this.flipperServer.pluginManager.stopAllServerAddOns(this.info.serial);
|
this.flipperServer.pluginManager.stopAllServerAddOns(this.info.serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshotAvailable(): Promise<boolean> {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
screenshot(): Promise<Buffer> {
|
screenshot(): Promise<Buffer> {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error('No screenshot support for current device'),
|
new Error('No screenshot support for current device'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenCaptureAvailable(): Promise<boolean> {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async startScreenCapture(_destination: string): Promise<void> {
|
async startScreenCapture(_destination: string): Promise<void> {
|
||||||
throw new Error('startScreenCapture not implemented on BaseDevice ');
|
throw new Error('startScreenCapture not implemented on BaseDevice ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export default class AndroidDevice extends ServerDevice {
|
|||||||
specs,
|
specs,
|
||||||
abiList,
|
abiList,
|
||||||
sdkVersion,
|
sdkVersion,
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.adb = adb;
|
this.adb = adb;
|
||||||
|
|
||||||
@@ -115,7 +119,7 @@ export default class AndroidDevice extends ServerDevice {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenCaptureAvailable(): Promise<boolean> {
|
async screenRecordAvailable(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.executeShellOrDie(
|
await this.executeShellOrDie(
|
||||||
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,
|
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,
|
||||||
|
|||||||
@@ -26,10 +26,4 @@ export default class KaiOSDevice extends AndroidDevice {
|
|||||||
'KaiOS',
|
'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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
resolve(androidLikeDevice);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export default class MacDevice extends ServerDevice {
|
|||||||
title: 'Mac',
|
title: 'Mac',
|
||||||
os: 'MacOS',
|
os: 'MacOS',
|
||||||
icon: 'app-apple',
|
icon: 'app-apple',
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export default class WindowsDevice extends ServerDevice {
|
|||||||
title: 'Windows',
|
title: 'Windows',
|
||||||
os: 'Windows',
|
os: 'Windows',
|
||||||
icon: 'app-microsoft-windows',
|
icon: 'app-microsoft-windows',
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export default class IOSDevice extends ServerDevice {
|
|||||||
title,
|
title,
|
||||||
os: 'iOS',
|
os: 'iOS',
|
||||||
icon: 'mobile',
|
icon: 'mobile',
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: true,
|
||||||
|
screenshotAvailable: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.buffer = '';
|
this.buffer = '';
|
||||||
this.iOSBridge = iOSBridge;
|
this.iOSBridge = iOSBridge;
|
||||||
@@ -76,10 +80,6 @@ export default class IOSDevice extends ServerDevice {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenCaptureAvailable() {
|
|
||||||
return this.connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
async startScreenCapture(destination: string) {
|
async startScreenCapture(destination: string) {
|
||||||
const recording = this.recording;
|
const recording = this.recording;
|
||||||
if (recording) {
|
if (recording) {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ export default class MetroDevice extends ServerDevice {
|
|||||||
title: 'React Native',
|
title: 'React Native',
|
||||||
os: 'Metro',
|
os: 'Metro',
|
||||||
icon: 'mobile',
|
icon: 'mobile',
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (ws) {
|
if (ws) {
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Button as AntButton, message} from 'antd';
|
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 {capture, getCaptureLocation, getFileName} from '../utils/screenshot';
|
||||||
import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons';
|
import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons';
|
||||||
import {useStore} from '../utils/useStore';
|
import {useStore} from '../utils/useStore';
|
||||||
@@ -22,21 +22,8 @@ async function openFile(path: string) {
|
|||||||
export default function ScreenCaptureButtons() {
|
export default function ScreenCaptureButtons() {
|
||||||
const selectedDevice = useStore((state) => state.connections.selectedDevice);
|
const selectedDevice = useStore((state) => state.connections.selectedDevice);
|
||||||
const [isTakingScreenshot, setIsTakingScreenshot] = useState(false);
|
const [isTakingScreenshot, setIsTakingScreenshot] = useState(false);
|
||||||
const [isRecordingAvailable, setIsRecordingAvailable] = useState(false);
|
|
||||||
const [isRecording, setIsRecording] = 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(() => {
|
const handleScreenshot = useCallback(() => {
|
||||||
setIsTakingScreenshot(true);
|
setIsTakingScreenshot(true);
|
||||||
return capture(selectedDevice!)
|
return capture(selectedDevice!)
|
||||||
@@ -87,7 +74,10 @@ export default function ScreenCaptureButtons() {
|
|||||||
title="Take Screenshot"
|
title="Take Screenshot"
|
||||||
type="ghost"
|
type="ghost"
|
||||||
onClick={handleScreenshot}
|
onClick={handleScreenshot}
|
||||||
disabled={!selectedDevice}
|
disabled={
|
||||||
|
!selectedDevice ||
|
||||||
|
!selectedDevice.description.features.screenshotAvailable
|
||||||
|
}
|
||||||
loading={isTakingScreenshot}
|
loading={isTakingScreenshot}
|
||||||
/>
|
/>
|
||||||
<AntButton
|
<AntButton
|
||||||
@@ -95,7 +85,10 @@ export default function ScreenCaptureButtons() {
|
|||||||
title="Make Screen Recording"
|
title="Make Screen Recording"
|
||||||
type={isRecording ? 'primary' : 'ghost'}
|
type={isRecording ? 'primary' : 'ghost'}
|
||||||
onClick={handleRecording}
|
onClick={handleRecording}
|
||||||
disabled={!selectedDevice || !isRecordingAvailable}
|
disabled={
|
||||||
|
!selectedDevice ||
|
||||||
|
!selectedDevice.description.features.screenCaptureAvailable
|
||||||
|
}
|
||||||
danger={isRecording}
|
danger={isRecording}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ type DispatchFromProps = {};
|
|||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
recording: boolean;
|
recording: boolean;
|
||||||
recordingEnabled: boolean;
|
|
||||||
};
|
};
|
||||||
type Props = OwnProps & StateFromProps & DispatchFromProps;
|
type Props = OwnProps & StateFromProps & DispatchFromProps;
|
||||||
|
|
||||||
export default class VideoRecordingButton extends Component<Props, State> {
|
export default class VideoRecordingButton extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
recording: false,
|
recording: false,
|
||||||
recordingEnabled: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
startRecording = async () => {
|
startRecording = async () => {
|
||||||
@@ -80,7 +78,6 @@ export default class VideoRecordingButton extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
render() {
|
render() {
|
||||||
const {recordingEnabled} = this.state;
|
|
||||||
const {selectedDevice} = this.props;
|
const {selectedDevice} = this.props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -89,7 +86,10 @@ export default class VideoRecordingButton extends Component<Props, State> {
|
|||||||
pulse={this.state.recording}
|
pulse={this.state.recording}
|
||||||
selected={this.state.recording}
|
selected={this.state.recording}
|
||||||
title="Make Screen Recording"
|
title="Make Screen Recording"
|
||||||
disabled={!selectedDevice || !recordingEnabled}
|
disabled={
|
||||||
|
!selectedDevice ||
|
||||||
|
!selectedDevice.description.features.screenCaptureAvailable
|
||||||
|
}
|
||||||
type={this.state.recording ? 'danger' : 'primary'}>
|
type={this.state.recording ? 'danger' : 'primary'}>
|
||||||
<Glyph
|
<Glyph
|
||||||
name={this.state.recording ? 'stop-playback' : 'camcorder'}
|
name={this.state.recording ? 'stop-playback' : 'camcorder'}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export default class ArchivedDevice extends BaseDevice {
|
|||||||
os: options.os,
|
os: options.os,
|
||||||
serial: options.serial,
|
serial: options.serial,
|
||||||
icon: 'box',
|
icon: 'box',
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.connected.set(false);
|
this.connected.set(false);
|
||||||
|
|||||||
@@ -235,32 +235,15 @@ export default class BaseDevice implements Device {
|
|||||||
return this.flipperServer.exec('device-navigate', this.serial, location);
|
return this.flipperServer.exec('device-navigate', this.serial, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshotAvailable(): Promise<boolean> {
|
async screenshot(): Promise<Uint8Array | undefined> {
|
||||||
if (this.isArchived) {
|
if (!this.description.features.screenshotAvailable || this.isArchived) {
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
return this.flipperServer.exec('device-supports-screenshot', this.serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
async screenshot(): Promise<Uint8Array> {
|
|
||||||
if (this.isArchived) {
|
|
||||||
return new Uint8Array();
|
|
||||||
}
|
}
|
||||||
return Base64.toUint8Array(
|
return Base64.toUint8Array(
|
||||||
await this.flipperServer.exec('device-take-screenshot', this.serial),
|
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> {
|
async startScreenCapture(destination: string): Promise<void> {
|
||||||
return this.flipperServer.exec(
|
return this.flipperServer.exec(
|
||||||
'device-start-screencapture',
|
'device-start-screencapture',
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export class TestDevice extends BaseDevice {
|
|||||||
title,
|
title,
|
||||||
os,
|
os,
|
||||||
specs,
|
specs,
|
||||||
|
features: {
|
||||||
|
screenCaptureAvailable: false,
|
||||||
|
screenshotAvailable: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import BaseDevice from '../devices/BaseDevice';
|
|||||||
import {reportPlatformFailures} from 'flipper-common';
|
import {reportPlatformFailures} from 'flipper-common';
|
||||||
import {getRenderHostInstance} from '../RenderHost';
|
import {getRenderHostInstance} from '../RenderHost';
|
||||||
import {getFlipperLib, path} from 'flipper-plugin';
|
import {getFlipperLib, path} from 'flipper-plugin';
|
||||||
|
import {assertNotNull} from './assertNotNull';
|
||||||
|
|
||||||
export function getCaptureLocation() {
|
export function getCaptureLocation() {
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +28,7 @@ export function getFileName(extension: 'png' | 'mp4'): string {
|
|||||||
|
|
||||||
export async function capture(device: BaseDevice): Promise<string> {
|
export async function capture(device: BaseDevice): Promise<string> {
|
||||||
if (!device.connected.get()) {
|
if (!device.connected.get()) {
|
||||||
console.log('Skipping screenshot for disconnected device');
|
console.info('Skipping screenshot for disconnected device');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const pngPath = path.join(getCaptureLocation(), getFileName('png'));
|
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`
|
// again to write in a file, probably easier to change screenshot api to `device.screenshot(): path`
|
||||||
device
|
device
|
||||||
.screenshot()
|
.screenshot()
|
||||||
.then((buffer) =>
|
.then((buffer) => {
|
||||||
getFlipperLib().remoteServerContext.fs.writeFileBinary(pngPath, buffer),
|
assertNotNull(
|
||||||
)
|
buffer,
|
||||||
|
`Device ${device.description.deviceType}:${device.description.os} does not support taking screenshots`,
|
||||||
|
);
|
||||||
|
return getFlipperLib().remoteServerContext.fs.writeFileBinary(
|
||||||
|
pngPath,
|
||||||
|
buffer,
|
||||||
|
);
|
||||||
|
})
|
||||||
.then(() => pngPath),
|
.then(() => pngPath),
|
||||||
'captureScreenshot',
|
'captureScreenshot',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const screenshot = await client.device.screenshot();
|
const screenshot = await client.device.screenshot();
|
||||||
if (screenshot.byteLength === 0) {
|
if (!screenshot) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[navigation] Could not retrieve valid screenshot from the device.',
|
'[navigation] Could not retrieve valid screenshot from the device.',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user