feat(adb): add power related api

This commit is contained in:
Simon Chan 2022-01-13 17:28:07 +08:00
parent 1ea248d57e
commit 45d784c8a5
19 changed files with 196 additions and 106 deletions

View file

@ -6,7 +6,7 @@ import AdbWsBackend from '@yume-chan/adb-backend-ws';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'; import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { global, logger } from '../state'; import { globalState, logger } from '../state';
import { CommonStackTokens, Icons } from '../utils'; import { CommonStackTokens, Icons } from '../utils';
const DropdownStyles = { dropdown: { width: '100%' } }; const DropdownStyles = { dropdown: { width: '100%' } };
@ -33,7 +33,7 @@ function _Connect(): JSX.Element | null {
setSupported(supported); setSupported(supported);
if (!supported) { if (!supported) {
global.showErrorDialog('Your browser does not support WebUSB standard, which is required for this site to work.\n\nLatest version of Google Chrome, Microsoft Edge, or other Chromium-based browsers are required.'); globalState.showErrorDialog('Your browser does not support WebUSB standard, which is required for this site to work.\n\nLatest version of Google Chrome, Microsoft Edge, or other Chromium-based browsers are required.');
return; return;
} }
@ -134,24 +134,24 @@ function _Connect(): JSX.Element | null {
try { try {
setConnecting(true); setConnecting(true);
await device.connect(CredentialStore); await device.connect(CredentialStore);
global.setDevice(device); globalState.setDevice(device);
} catch (e) { } catch (e) {
device.dispose(); device.dispose();
throw e; throw e;
} }
} }
} catch (e: any) { } catch (e: any) {
global.showErrorDialog(e.message); globalState.showErrorDialog(e.message);
} finally { } finally {
setConnecting(false); setConnecting(false);
} }
}, [selectedBackend]); }, [selectedBackend]);
const disconnect = useCallback(async () => { const disconnect = useCallback(async () => {
try { try {
await global.device!.dispose(); await globalState.device!.dispose();
global.setDevice(undefined); globalState.setDevice(undefined);
} catch (e: any) { } catch (e: any) {
global.showErrorDialog(e.message); globalState.showErrorDialog(e.message);
} }
}, []); }, []);
@ -214,7 +214,7 @@ function _Connect(): JSX.Element | null {
tokens={{ childrenGap: 8, padding: '0 0 8px 8px' }} tokens={{ childrenGap: 8, padding: '0 0 8px 8px' }}
> >
<Dropdown <Dropdown
disabled={!!global.device || backendOptions.length === 0} disabled={!!globalState.device || backendOptions.length === 0}
label="Available devices" label="Available devices"
placeholder="No available devices" placeholder="No available devices"
options={backendOptions} options={backendOptions}
@ -224,7 +224,7 @@ function _Connect(): JSX.Element | null {
onChange={handleSelectedBackendChange} onChange={handleSelectedBackendChange}
/> />
{!global.device {!globalState.device
? ( ? (
<Stack horizontal tokens={CommonStackTokens}> <Stack horizontal tokens={CommonStackTokens}>
<StackItem grow shrink> <StackItem grow shrink>

View file

@ -3,7 +3,7 @@ import { AdbDemoModeMobileDataType, AdbDemoModeMobileDataTypes, AdbDemoModeSigna
import { autorun, makeAutoObservable, reaction, runInAction } from "mobx"; import { autorun, makeAutoObservable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { CSSProperties, useCallback } from 'react'; import { CSSProperties, useCallback } from 'react';
import { global } from "../state"; import { globalState } from "../state";
const SignalStrengthOptions = const SignalStrengthOptions =
Object.values(AdbDemoModeSignalStrength) Object.values(AdbDemoModeSignalStrength)
@ -65,7 +65,7 @@ class DemoModeState {
makeAutoObservable(this); makeAutoObservable(this);
reaction( reaction(
() => global.device, () => globalState.device,
async (device) => { async (device) => {
if (device) { if (device) {
const allowed = await device.demoMode.getAllowed(); const allowed = await device.demoMode.getAllowed();
@ -120,21 +120,21 @@ const FEATURES: FeatureDefinition[][] = [
max: 100, max: 100,
step: 1, step: 1,
initial: 100, initial: 100,
onChange: (value) => global.device!.demoMode.setBatteryLevel(value as number), onChange: (value) => globalState.device!.demoMode.setBatteryLevel(value as number),
}, },
{ {
key: 'batteryCharging', key: 'batteryCharging',
label: 'Battery Charging', label: 'Battery Charging',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setBatteryCharging(value as boolean), onChange: (value) => globalState.device!.demoMode.setBatteryCharging(value as boolean),
}, },
{ {
key: 'powerSaveMode', key: 'powerSaveMode',
label: 'Power Save Mode', label: 'Power Save Mode',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setPowerSaveMode(value as boolean), onChange: (value) => globalState.device!.demoMode.setPowerSaveMode(value as boolean),
}, },
], ],
[ [
@ -144,14 +144,14 @@ const FEATURES: FeatureDefinition[][] = [
type: 'select', type: 'select',
options: SignalStrengthOptions, options: SignalStrengthOptions,
initial: AdbDemoModeSignalStrength.Level4, initial: AdbDemoModeSignalStrength.Level4,
onChange: (value) => global.device!.demoMode.setWifiSignalStrength(value as AdbDemoModeSignalStrength), onChange: (value) => globalState.device!.demoMode.setWifiSignalStrength(value as AdbDemoModeSignalStrength),
}, },
{ {
key: 'airplaneMode', key: 'airplaneMode',
label: 'Airplane Mode', label: 'Airplane Mode',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setAirplaneMode(value as boolean), onChange: (value) => globalState.device!.demoMode.setAirplaneMode(value as boolean),
}, },
{ {
key: 'mobileDataType', key: 'mobileDataType',
@ -159,7 +159,7 @@ const FEATURES: FeatureDefinition[][] = [
type: 'select', type: 'select',
options: MobileDataTypeOptions, options: MobileDataTypeOptions,
initial: 'lte', initial: 'lte',
onChange: (value) => global.device!.demoMode.setMobileDataType(value as AdbDemoModeMobileDataType), onChange: (value) => globalState.device!.demoMode.setMobileDataType(value as AdbDemoModeMobileDataType),
}, },
{ {
key: 'mobileSignalStrength', key: 'mobileSignalStrength',
@ -167,7 +167,7 @@ const FEATURES: FeatureDefinition[][] = [
type: 'select', type: 'select',
options: SignalStrengthOptions, options: SignalStrengthOptions,
initial: AdbDemoModeSignalStrength.Level4, initial: AdbDemoModeSignalStrength.Level4,
onChange: (value) => global.device!.demoMode.setMobileSignalStrength(value as AdbDemoModeSignalStrength), onChange: (value) => globalState.device!.demoMode.setMobileSignalStrength(value as AdbDemoModeSignalStrength),
}, },
], ],
[ [
@ -177,42 +177,42 @@ const FEATURES: FeatureDefinition[][] = [
type: 'select', type: 'select',
options: StatusBarModeOptions, options: StatusBarModeOptions,
initial: 'transparent', initial: 'transparent',
onChange: (value) => global.device!.demoMode.setStatusBarMode(value as AdbDemoModeStatusBarMode), onChange: (value) => globalState.device!.demoMode.setStatusBarMode(value as AdbDemoModeStatusBarMode),
}, },
{ {
key: 'vibrateMode', key: 'vibrateMode',
label: 'Vibrate Mode Indicator', label: 'Vibrate Mode Indicator',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setVibrateModeEnabled(value as boolean), onChange: (value) => globalState.device!.demoMode.setVibrateModeEnabled(value as boolean),
}, },
{ {
key: 'bluetoothConnected', key: 'bluetoothConnected',
label: 'Bluetooth Indicator', label: 'Bluetooth Indicator',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setBluetoothConnected(value as boolean), onChange: (value) => globalState.device!.demoMode.setBluetoothConnected(value as boolean),
}, },
{ {
key: 'locatingIcon', key: 'locatingIcon',
label: 'Locating Icon', label: 'Locating Icon',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setLocatingIcon(value as boolean), onChange: (value) => globalState.device!.demoMode.setLocatingIcon(value as boolean),
}, },
{ {
key: 'alarmIcon', key: 'alarmIcon',
label: 'Alarm Icon', label: 'Alarm Icon',
type: 'boolean', type: 'boolean',
initial: false, initial: false,
onChange: (value) => global.device!.demoMode.setAlarmIcon(value as boolean), onChange: (value) => globalState.device!.demoMode.setAlarmIcon(value as boolean),
}, },
{ {
key: 'notificationsVisibility', key: 'notificationsVisibility',
label: 'Notifications Visibility', label: 'Notifications Visibility',
type: 'boolean', type: 'boolean',
initial: true, initial: true,
onChange: (value) => global.device!.demoMode.setNotificationsVisibility(value as boolean), onChange: (value) => globalState.device!.demoMode.setNotificationsVisibility(value as boolean),
}, },
{ {
key: 'hour', key: 'hour',
@ -222,7 +222,7 @@ const FEATURES: FeatureDefinition[][] = [
max: 23, max: 23,
step: 1, step: 1,
initial: 12, initial: 12,
onChange: (value) => global.device!.demoMode.setTime(value as number, state.features.get('minute') as number | undefined ?? 34) onChange: (value) => globalState.device!.demoMode.setTime(value as number, state.features.get('minute') as number | undefined ?? 34)
}, },
{ {
key: 'minute', key: 'minute',
@ -232,7 +232,7 @@ const FEATURES: FeatureDefinition[][] = [
max: 59, max: 59,
step: 1, step: 1,
initial: 34, initial: 34,
onChange: (value) => global.device!.demoMode.setTime(state.features.get('hour') as number | undefined ?? 34, value as number) onChange: (value) => globalState.device!.demoMode.setTime(state.features.get('hour') as number | undefined ?? 34, value as number)
}, },
], ],
]; ];
@ -306,7 +306,7 @@ const DemoModeBase = ({
style, style,
}: DemoModeProps) => { }: DemoModeProps) => {
const handleAllowedChange = useCallback(async (e, value?: boolean) => { const handleAllowedChange = useCallback(async (e, value?: boolean) => {
await global.device!.demoMode.setAllowed(value!); await globalState.device!.demoMode.setAllowed(value!);
runInAction(() => { runInAction(() => {
state.allowed = value!; state.allowed = value!;
state.enabled = false; state.enabled = false;
@ -314,7 +314,7 @@ const DemoModeBase = ({
}, []); }, []);
const handleEnabledChange = useCallback(async (e, value?: boolean) => { const handleEnabledChange = useCallback(async (e, value?: boolean) => {
await global.device!.demoMode.setEnabled(value!); await globalState.device!.demoMode.setEnabled(value!);
runInAction(() => state.enabled = value!); runInAction(() => state.enabled = value!);
}, []); }, []);
@ -322,7 +322,7 @@ const DemoModeBase = ({
<div style={{ padding: 12, overflow: 'hidden auto', ...style }}> <div style={{ padding: 12, overflow: 'hidden auto', ...style }}>
<Toggle <Toggle
label="Allowed" label="Allowed"
disabled={!global.device} disabled={!globalState.device}
checked={state.allowed} checked={state.allowed}
onChange={handleAllowedChange} onChange={handleAllowedChange}
/> />

View file

@ -1,7 +1,7 @@
import { Dialog, DialogFooter, DialogType, PrimaryButton } from '@fluentui/react'; import { Dialog, DialogFooter, DialogType, PrimaryButton } from '@fluentui/react';
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { global } from '../state'; import { globalState } from '../state';
export const ErrorDialogProvider = observer((props: PropsWithChildren<{}>) => { export const ErrorDialogProvider = observer((props: PropsWithChildren<{}>) => {
return ( return (
@ -9,15 +9,15 @@ export const ErrorDialogProvider = observer((props: PropsWithChildren<{}>) => {
{props.children} {props.children}
<Dialog <Dialog
hidden={!global.errorDialogVisible} hidden={!globalState.errorDialogVisible}
dialogContentProps={{ dialogContentProps={{
type: DialogType.normal, type: DialogType.normal,
title: 'Error', title: 'Error',
subText: global.errorDialogMessage, subText: globalState.errorDialogMessage,
}} }}
> >
<DialogFooter> <DialogFooter>
<PrimaryButton text="OK" onClick={global.hideErrorDialog} /> <PrimaryButton text="OK" onClick={globalState.hideErrorDialog} />
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>
</> </>

View file

@ -3,7 +3,7 @@ import { AdbPacketInit, decodeUtf8 } from '@yume-chan/adb';
import { DisposableList } from '@yume-chan/event'; import { DisposableList } from '@yume-chan/event';
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { global, logger } from "../state"; import { globalState, logger } from "../state";
import { Icons, withDisplayName } from '../utils'; import { Icons, withDisplayName } from '../utils';
import { CommandBar } from './command-bar'; import { CommandBar } from './command-bar';
@ -57,10 +57,10 @@ const LogLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, Ad
export const ToggleLogView = observer(() => { export const ToggleLogView = observer(() => {
return ( return (
<IconButton <IconButton
checked={global.logVisible} checked={globalState.logVisible}
iconProps={{ iconName: Icons.TextGrammarError }} iconProps={{ iconName: Icons.TextGrammarError }}
title="Toggle Log" title="Toggle Log"
onClick={global.toggleLog} onClick={globalState.toggleLog}
/> />
); );
}); });
@ -142,7 +142,7 @@ export const LogView = observer(({
classNames['logger-container'], classNames['logger-container'],
), [className]); ), [className]);
if (!global.logVisible) { if (!globalState.logVisible) {
return null; return null;
} }

View file

@ -51,6 +51,11 @@ const ROUTES = [
icon: Icons.Box, icon: Icons.Box,
name: 'Install APK', name: 'Install APK',
}, },
{
url: '/power',
icon: Icons.Power,
name: 'Power Menu',
},
]; ];
function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps<INavButtonProps>) { function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps<INavButtonProps>) {

View file

@ -5,7 +5,7 @@ import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from "react"; import React from "react";
import { ExternalLink } from "../components"; import { ExternalLink } from "../components";
import { global } from '../state'; import { globalState } from '../state';
import { Icons, RouteStackProps } from "../utils"; import { Icons, RouteStackProps } from "../utils";
const KNOWN_FEATURES: Record<string, string> = { const KNOWN_FEATURES: Record<string, string> = {
@ -52,7 +52,7 @@ const DeviceInfo: NextPage = () => {
</MessageBar> </MessageBar>
<span> <span>
<span>Protocol Version: </span> <span>Protocol Version: </span>
<code>{global.device?.protocolVersion?.toString(16).padStart(8, '0')}</code> <code>{globalState.device?.protocolVersion?.toString(16).padStart(8, '0')}</code>
</span> </span>
<Separator /> <Separator />
@ -60,21 +60,21 @@ const DeviceInfo: NextPage = () => {
<code>ro.product.name</code> <code>ro.product.name</code>
<span> field in Android Build Props</span> <span> field in Android Build Props</span>
</MessageBar> </MessageBar>
<span>Product Name: {global.device?.product}</span> <span>Product Name: {globalState.device?.product}</span>
<Separator /> <Separator />
<MessageBar> <MessageBar>
<code>ro.product.model</code> <code>ro.product.model</code>
<span> field in Android Build Props</span> <span> field in Android Build Props</span>
</MessageBar> </MessageBar>
<span>Model Name: {global.device?.model}</span> <span>Model Name: {globalState.device?.model}</span>
<Separator /> <Separator />
<MessageBar> <MessageBar>
<code>ro.product.device</code> <code>ro.product.device</code>
<span> field in Android Build Props</span> <span> field in Android Build Props</span>
</MessageBar> </MessageBar>
<span>Device Name: {global.device?.device}</span> <span>Device Name: {globalState.device?.device}</span>
<Separator /> <Separator />
<MessageBar> <MessageBar>
@ -87,7 +87,7 @@ const DeviceInfo: NextPage = () => {
</MessageBar> </MessageBar>
<span> <span>
<span>Features: </span> <span>Features: </span>
{global.device?.features?.map((feature, index) => ( {globalState.device?.features?.map((feature, index) => (
<span key={feature}> <span key={feature}>
{index !== 0 && (<span>, </span>)} {index !== 0 && (<span>, </span>)}
<span>{feature}</span> <span>{feature}</span>

View file

@ -12,7 +12,7 @@ import Router, { useRouter } from "next/router";
import path from 'path'; import path from 'path';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { CommandBar } from '../components'; import { CommandBar } from '../components';
import { global } from '../state'; import { globalState } from '../state';
import { asyncEffect, chunkFile, formatSize, formatSpeed, Icons, pickFile, RouteStackProps } from '../utils'; import { asyncEffect, chunkFile, formatSize, formatSpeed, Icons, pickFile, RouteStackProps } from '../utils';
let StreamSaver: typeof import('streamsaver'); let StreamSaver: typeof import('streamsaver');
@ -140,7 +140,7 @@ class FileManagerState {
iconName: Icons.CloudArrowUp, iconName: Icons.CloudArrowUp,
style: { height: 20, fontSize: 20, lineHeight: 1.5 } style: { height: 20, fontSize: 20, lineHeight: 1.5 }
}, },
disabled: !global.device, disabled: !globalState.device,
onClick: () => { onClick: () => {
(async () => { (async () => {
const files = await pickFile({ multiple: true }); const files = await pickFile({ multiple: true });
@ -167,7 +167,7 @@ class FileManagerState {
}, },
onClick: () => { onClick: () => {
(async () => { (async () => {
const sync = await global.device!.sync(); const sync = await globalState.device!.sync();
try { try {
const itemPath = path.resolve(this.path, this.selectedItems[0].name!); const itemPath = path.resolve(this.path, this.selectedItems[0].name!);
const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath)); const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath));
@ -177,7 +177,7 @@ class FileManagerState {
}); });
await readableStream.pipeTo(writeableStream); await readableStream.pipeTo(writeableStream);
} catch (e) { } catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`); globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally { } finally {
sync.dispose(); sync.dispose();
} }
@ -199,14 +199,14 @@ class FileManagerState {
(async () => { (async () => {
try { try {
for (const item of this.selectedItems) { for (const item of this.selectedItems) {
const output = await global.device!.rm(path.resolve(this.path, item.name!)); const output = await globalState.device!.rm(path.resolve(this.path, item.name!));
if (output) { if (output) {
global.showErrorDialog(output); globalState.showErrorDialog(output);
return; return;
} }
} }
} catch (e) { } catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`); globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally { } finally {
this.loadFiles(); this.loadFiles();
} }
@ -359,7 +359,7 @@ class FileManagerState {
}); });
autorun(() => { autorun(() => {
if (global.device) { if (globalState.device) {
if (this.initial && this.visible) { if (this.initial && this.visible) {
this.initial = false; this.initial = false;
this.loadFiles(); this.loadFiles();
@ -381,7 +381,7 @@ class FileManagerState {
this.path = path; this.path = path;
if (!global.device) { if (!globalState.device) {
return; return;
} }
@ -393,13 +393,13 @@ class FileManagerState {
runInAction(() => this.items = []); runInAction(() => this.items = []);
if (!global.device) { if (!globalState.device) {
return; return;
} }
runInAction(() => this.loading = true); runInAction(() => this.loading = true);
const sync = await global.device.sync(); const sync = await globalState.device.sync();
const items: ListItem[] = []; const items: ListItem[] = [];
const linkItems: AdbSyncEntryResponse[] = []; const linkItems: AdbSyncEntryResponse[] = [];
@ -456,7 +456,7 @@ class FileManagerState {
}); });
upload = async (file: File) => { upload = async (file: File) => {
const sync = await global.device!.sync(); const sync = await globalState.device!.sync();
try { try {
const itemPath = path.resolve(this.path!, file.name); const itemPath = path.resolve(this.path!, file.name);
runInAction(() => { runInAction(() => {
@ -491,7 +491,7 @@ class FileManagerState {
clearInterval(intervalId); clearInterval(intervalId);
} }
} catch (e) { } catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`); globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally { } finally {
sync.dispose(); sync.dispose();
this.loadFiles(); this.loadFiles();
@ -566,7 +566,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
const [previewUrl, setPreviewUrl] = useState<string | undefined>(); const [previewUrl, setPreviewUrl] = useState<string | undefined>();
const previewImage = useCallback(async (path: string) => { const previewImage = useCallback(async (path: string) => {
const sync = await global.device!.sync(); const sync = await globalState.device!.sync();
try { try {
const readableStream = createReadableStreamFromBufferIterator(sync.read(path)); const readableStream = createReadableStreamFromBufferIterator(sync.read(path));
const response = new Response(readableStream); const response = new Response(readableStream);

View file

@ -6,7 +6,7 @@ import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { CommandBar, DemoMode, DeviceView } from '../components'; import { CommandBar, DemoMode, DeviceView } from '../components';
import { global } from "../state"; import { globalState } from "../state";
import { Icons, RouteStackProps } from "../utils"; import { Icons, RouteStackProps } from "../utils";
class FrameBufferState { class FrameBufferState {
@ -38,15 +38,15 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const capture = useCallback(async () => { const capture = useCallback(async () => {
if (!global.device) { if (!globalState.device) {
return; return;
} }
try { try {
const framebuffer = await global.device.framebuffer(); const framebuffer = await globalState.device.framebuffer();
state.setImage(framebuffer); state.setImage(framebuffer);
} catch (e) { } catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`); globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} }
}, []); }, []);
@ -65,7 +65,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
const commandBarItems = computed(() => [ const commandBarItems = computed(() => [
{ {
key: 'start', key: 'start',
disabled: !global.device, disabled: !globalState.device,
iconProps: { iconName: Icons.Camera, style: { height: 20, fontSize: 20, lineHeight: 1.5 } }, iconProps: { iconName: Icons.Camera, style: { height: 20, fontSize: 20, lineHeight: 1.5 } },
text: 'Capture', text: 'Capture',
onClick: capture, onClick: capture,
@ -84,7 +84,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
const url = canvas.toDataURL(); const url = canvas.toDataURL();
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `Screenshot of ${global.device!.name}.png`; a.download = `Screenshot of ${globalState.device!.name}.png`;
a.click(); a.click();
}, },
}, },

View file

@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { NextPage } from "next"; import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import React from "react"; import React from "react";
import { global } from "../state"; import { globalState } from "../state";
import { chunkFile, pickFile, RouteStackProps } from "../utils"; import { chunkFile, pickFile, RouteStackProps } from "../utils";
enum Stage { enum Stage {
@ -57,7 +57,7 @@ class InstallPageState {
}; };
}); });
await global.device!.install(chunkFile(file, AdbSyncMaxPacketSize), uploaded => { await globalState.device!.install(chunkFile(file, AdbSyncMaxPacketSize), uploaded => {
runInAction(() => { runInAction(() => {
if (uploaded !== file.size) { if (uploaded !== file.size) {
this.progress = { this.progress = {
@ -103,7 +103,7 @@ const Install: NextPage = () => {
<Stack horizontal> <Stack horizontal>
<DefaultButton <DefaultButton
disabled={!global.device || state.installing} disabled={!globalState.device || state.installing}
text="Open" text="Open"
onClick={state.install} onClick={state.install}
/> />

48
apps/demo/pages/power.tsx Normal file
View file

@ -0,0 +1,48 @@
import { DefaultButton, Icon, MessageBar, TooltipHost } from "@fluentui/react";
import { observer } from "mobx-react-lite";
import { NextPage } from "next";
import { globalState } from "../state";
import { Icons } from "../utils";
const Power: NextPage = () => {
return (
<div style={{ padding: 20 }}>
<div>
<DefaultButton text="Reboot" disabled={!globalState.device} onClick={() => globalState.device!.power.reboot()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Reboot to Bootloader" disabled={!globalState.device} onClick={() => globalState.device!.power.bootloader()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Reboot to Fastboot" disabled={!globalState.device} onClick={() => globalState.device!.power.fastboot()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Reboot to Recovery" disabled={!globalState.device} onClick={() => globalState.device!.power.recovery()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Reboot to Sideload" disabled={!globalState.device} onClick={() => globalState.device!.power.sideload()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Reboot to Qualcomm EDL Mode" disabled={!globalState.device} onClick={() => globalState.device!.power.qualcommEdlMode()} />
<TooltipHost content={<span>Only works on some Qualcomm devices.</span>}>
<Icon style={{ verticalAlign: 'middle', marginLeft: 4, fontSize: 18 }} iconName={Icons.Info} />
</TooltipHost>
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Power Off" disabled={!globalState.device} onClick={() => globalState.device!.power.powerOff()} />
</div>
<div style={{ marginTop: 20 }}>
<DefaultButton text="Press Power Button" disabled={!globalState.device} onClick={() => globalState.device!.power.powerButton()} />
</div>
</div>
);
};
export default observer(Power);

View file

@ -8,7 +8,7 @@ import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { DemoMode, DeviceView, DeviceViewRef, ExternalLink } from "../components"; import { DemoMode, DeviceView, DeviceViewRef, ExternalLink } from "../components";
import { global } from "../state"; import { globalState } from "../state";
import { CommonStackTokens, formatSpeed, Icons, RouteStackProps } from "../utils"; import { CommonStackTokens, formatSpeed, Icons, RouteStackProps } from "../utils";
import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version'; import SCRCPY_SERVER_VERSION from '@yume-chan/scrcpy/bin/version';
@ -201,7 +201,7 @@ class ScrcpyPageState {
if (!this.running) { if (!this.running) {
result.push({ result.push({
key: 'start', key: 'start',
disabled: !global.device, disabled: !globalState.device,
iconProps: { iconName: Icons.Play }, iconProps: { iconName: Icons.Play },
text: 'Start', text: 'Start',
onClick: this.start as VoidFunction, onClick: this.start as VoidFunction,
@ -310,7 +310,7 @@ class ScrcpyPageState {
}); });
autorun(() => { autorun(() => {
if (global.device) { if (globalState.device) {
runInAction(() => { runInAction(() => {
this.encoders = []; this.encoders = [];
this.selectedEncoder = undefined; this.selectedEncoder = undefined;
@ -351,7 +351,7 @@ class ScrcpyPageState {
} }
start = async () => { start = async () => {
if (!global.device) { if (!globalState.device) {
return; return;
} }
@ -395,7 +395,7 @@ class ScrcpyPageState {
}), 1000); }), 1000);
try { try {
await pushServer(global.device, serverBuffer, { await pushServer(globalState.device, serverBuffer, {
onProgress: action((progress) => { onProgress: action((progress) => {
this.serverUploadedSize = progress; this.serverUploadedSize = progress;
}), }),
@ -409,7 +409,7 @@ class ScrcpyPageState {
} }
const encoders = await ScrcpyClient.getEncoders( const encoders = await ScrcpyClient.getEncoders(
global.device, globalState.device,
DEFAULT_SERVER_PATH, DEFAULT_SERVER_PATH,
SCRCPY_SERVER_VERSION, SCRCPY_SERVER_VERSION,
new ScrcpyOptions1_21({ new ScrcpyOptions1_21({
@ -428,7 +428,7 @@ class ScrcpyPageState {
// Run scrcpy once will delete the server file // Run scrcpy once will delete the server file
// Re-push it // Re-push it
await pushServer(global.device, serverBuffer); await pushServer(globalState.device, serverBuffer);
const factory = this.selectedDecoder.factory; const factory = this.selectedDecoder.factory;
const decoder = new factory(); const decoder = new factory();
@ -436,7 +436,7 @@ class ScrcpyPageState {
this.decoder = decoder; this.decoder = decoder;
}); });
const client = new ScrcpyClient(global.device); const client = new ScrcpyClient(globalState.device);
runInAction(() => this.log = []); runInAction(() => this.log = []);
client.onOutput(action(line => this.log.push(line))); client.onOutput(action(line => this.log.push(line)));
client.onClose(this.stop); client.onClose(this.stop);
@ -487,7 +487,7 @@ class ScrcpyPageState {
this.running = true; this.running = true;
}); });
} catch (e: any) { } catch (e: any) {
global.showErrorDialog(e.message); globalState.showErrorDialog(e.message);
} finally { } finally {
runInAction(() => { runInAction(() => {
this.connecting = false; this.connecting = false;

View file

@ -5,7 +5,7 @@ import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
import { global } from "../state"; import { globalState } from "../state";
import { Icons, ResizeObserver, RouteStackProps } from '../utils'; import { Icons, ResizeObserver, RouteStackProps } from '../utils';
let terminal: import('../components/terminal').AdbTerminal; let terminal: import('../components/terminal').AdbTerminal;
@ -40,9 +40,9 @@ const Shell: NextPage = (): JSX.Element | null => {
const connectingRef = useRef(false); const connectingRef = useRef(false);
useEffect(() => { useEffect(() => {
return reaction( return reaction(
() => global.device, () => globalState.device,
async () => { async () => {
if (!global.device) { if (!globalState.device) {
terminal.socket = undefined; terminal.socket = undefined;
return; return;
} }
@ -53,10 +53,10 @@ const Shell: NextPage = (): JSX.Element | null => {
try { try {
connectingRef.current = true; connectingRef.current = true;
const socket = await global.device.childProcess.shell(); const socket = await globalState.device.childProcess.shell();
terminal.socket = socket; terminal.socket = socket;
} catch (e) { } catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`); globalState.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally { } finally {
connectingRef.current = false; connectingRef.current = false;
} }

View file

@ -5,7 +5,7 @@ import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { ExternalLink } from "../components"; import { ExternalLink } from "../components";
import { global } from "../state"; import { globalState } from "../state";
import { asyncEffect, Icons, RouteStackProps } from "../utils"; import { asyncEffect, Icons, RouteStackProps } from "../utils";
class TcpIpState { class TcpIpState {
@ -26,7 +26,7 @@ class TcpIpState {
autorun(() => { autorun(() => {
if (global.device) { if (globalState.device) {
if (this.initial && this.visible) { if (this.initial && this.visible) {
this.initial = false; this.initial = false;
this.queryInfo(); this.queryInfo();
@ -41,14 +41,14 @@ class TcpIpState {
return [ return [
{ {
key: 'refresh', key: 'refresh',
disabled: !global.device, disabled: !globalState.device,
iconProps: { iconName: Icons.ArrowClockwise }, iconProps: { iconName: Icons.ArrowClockwise },
text: 'Refresh', text: 'Refresh',
onClick: this.queryInfo as VoidFunction, onClick: this.queryInfo as VoidFunction,
}, },
{ {
key: 'apply', key: 'apply',
disabled: !global.device, disabled: !globalState.device,
iconProps: { iconName: Icons.Save }, iconProps: { iconName: Icons.Save },
text: 'Apply', text: 'Apply',
onClick: this.applyServicePort, onClick: this.applyServicePort,
@ -57,7 +57,7 @@ class TcpIpState {
} }
queryInfo = asyncEffect(async (signal) => { queryInfo = asyncEffect(async (signal) => {
if (!global.device) { if (!globalState.device) {
runInAction(() => { runInAction(() => {
this.serviceListenAddresses = undefined; this.serviceListenAddresses = undefined;
this.servicePortEnabled = false; this.servicePortEnabled = false;
@ -68,9 +68,9 @@ class TcpIpState {
return; return;
} }
const serviceListenAddresses = await global.device.getProp('service.adb.listen_addrs'); const serviceListenAddresses = await globalState.device.getProp('service.adb.listen_addrs');
const servicePort = await global.device.getProp('service.adb.tcp.port'); const servicePort = await globalState.device.getProp('service.adb.tcp.port');
const persistPort = await global.device.getProp('persist.adb.tcp.port'); const persistPort = await globalState.device.getProp('persist.adb.tcp.port');
if (signal.aborted) { if (signal.aborted) {
return; return;
@ -98,14 +98,14 @@ class TcpIpState {
}); });
applyServicePort = async () => { applyServicePort = async () => {
if (!global.device) { if (!globalState.device) {
return; return;
} }
if (state.servicePortEnabled) { if (state.servicePortEnabled) {
await global.device.tcpip.setPort(Number.parseInt(state.servicePort, 10)); await globalState.device.tcpip.setPort(Number.parseInt(state.servicePort, 10));
} else { } else {
await global.device.tcpip.disable(); await globalState.device.tcpip.disable();
} }
}; };
} }
@ -183,12 +183,12 @@ const TcpIp: NextPage = () => {
inlineLabel inlineLabel
label="service.adb.tcp.port" label="service.adb.tcp.port"
checked={state.servicePortEnabled} checked={state.servicePortEnabled}
disabled={!global.device || !!state.serviceListenAddresses} disabled={!globalState.device || !!state.serviceListenAddresses}
onText="Enabled" onText="Enabled"
offText="Disabled" offText="Disabled"
onChange={handleServicePortEnabledChange} onChange={handleServicePortEnabledChange}
/> />
{global && ( {globalState && (
<TextField <TextField
disabled={!!state.serviceListenAddresses} disabled={!!state.serviceListenAddresses}
value={state.servicePort} value={state.servicePort}

View file

@ -36,4 +36,4 @@ export class GlobalState {
} }
} }
export const global = new GlobalState(); export const globalState = new GlobalState();

View file

@ -1,5 +1,5 @@
import { registerIcons } from "@fluentui/react"; import { registerIcons } from "@fluentui/react";
import { MoreHorizontalRegular, AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, NavigationRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons'; import { AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, BoxRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, MoreHorizontalRegular, NavigationRegular, PersonFeedbackRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, PowerRegular, SaveRegular, SearchRegular, SettingsRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons';
const STYLE = {}; const STYLE = {};
@ -30,6 +30,7 @@ export function register() {
Play: <PlayRegular style={STYLE} />, Play: <PlayRegular style={STYLE} />,
PlugConnected: <PlugConnectedRegular style={STYLE} />, PlugConnected: <PlugConnectedRegular style={STYLE} />,
PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />, PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />,
Power: <PowerRegular style={STYLE} />,
Save: <SaveRegular style={STYLE} />, Save: <SaveRegular style={STYLE} />,
Settings: <SettingsRegular style={STYLE} />, Settings: <SettingsRegular style={STYLE} />,
Stop: <StopRegular style={STYLE} />, Stop: <StopRegular style={STYLE} />,
@ -77,6 +78,7 @@ export default {
Play: 'Play', Play: 'Play',
PlugConnected: 'PlugConnected', PlugConnected: 'PlugConnected',
PlugDisconnected: 'PlugDisconnected', PlugDisconnected: 'PlugDisconnected',
Power: 'Power',
Save: 'Save', Save: 'Save',
Settings: 'Settings', Settings: 'Settings',
Stop: 'Stop', Stop: 'Stop',

View file

@ -2,7 +2,7 @@ import { PromiseResolver } from '@yume-chan/async';
import { DisposableList } from '@yume-chan/event'; import { DisposableList } from '@yume-chan/event';
import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth'; import { AdbAuthenticationHandler, AdbCredentialStore, AdbDefaultAuthenticators } from './auth';
import { AdbBackend } from './backend'; import { AdbBackend } from './backend';
import { AdbChildProcess, AdbDemoMode, AdbFrameBuffer, AdbReverseCommand, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands'; import { AdbChildProcess, AdbDemoMode, AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install } from './commands';
import { AdbFeatures } from './features'; import { AdbFeatures } from './features';
import { AdbCommand } from './packet'; import { AdbCommand } from './packet';
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket'; import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
@ -44,22 +44,21 @@ export class Adb {
private _features: AdbFeatures[] | undefined; private _features: AdbFeatures[] | undefined;
public get features() { return this._features; } public get features() { return this._features; }
public readonly tcpip: AdbTcpIpCommand;
public readonly reverse: AdbReverseCommand;
public readonly demoMode: AdbDemoMode;
public readonly childProcess: AdbChildProcess; public readonly childProcess: AdbChildProcess;
public readonly demoMode: AdbDemoMode;
public readonly power: AdbPower;
public readonly reverse: AdbReverseCommand;
public readonly tcpip: AdbTcpIpCommand;
public constructor(backend: AdbBackend, logger?: AdbLogger) { public constructor(backend: AdbBackend, logger?: AdbLogger) {
this._backend = backend; this._backend = backend;
this.packetDispatcher = new AdbPacketDispatcher(backend, logger); this.packetDispatcher = new AdbPacketDispatcher(backend, logger);
this.tcpip = new AdbTcpIpCommand(this);
this.reverse = new AdbReverseCommand(this.packetDispatcher);
this.demoMode = new AdbDemoMode(this);
this.childProcess = new AdbChildProcess(this); this.childProcess = new AdbChildProcess(this);
this.demoMode = new AdbDemoMode(this);
this.power = new AdbPower(this);
this.reverse = new AdbReverseCommand(this.packetDispatcher);
this.tcpip = new AdbTcpIpCommand(this);
backend.onDisconnected(this.dispose, this); backend.onDisconnected(this.dispose, this);
} }

View file

@ -2,6 +2,7 @@ export * from './base';
export * from './demo-mode'; export * from './demo-mode';
export * from './framebuffer'; export * from './framebuffer';
export * from './install'; export * from './install';
export * from './power';
export * from './reverse'; export * from './reverse';
export * from './shell'; export * from './shell';
export * from './sync'; export * from './sync';

View file

@ -0,0 +1,35 @@
import { AdbCommandBase } from "./base";
export class AdbPower extends AdbCommandBase {
public reboot(name: string = '') {
return this.adb.createSocketAndReadAll(`reboot:${name}`);
}
public bootloader() {
return this.reboot('bootloader');
}
public fastboot() {
return this.reboot('fastboot');
}
public recovery() {
return this.reboot('recovery');
}
public sideload() {
return this.reboot('sideload');
}
public qualcommEdlMode() {
return this.reboot('edl');
}
public powerOff() {
return this.adb.childProcess.exec('reboot', '-p');
}
public powerButton(longPress: boolean = false) {
return this.adb.childProcess.exec('input', 'keyevent', longPress ? '--longpress POWER' : 'POWER');
}
}

View file

@ -51,7 +51,7 @@ export class AdbReverseCommand extends AutoDisposable {
} }
const address = decodeUtf8(e.packet.payload!); const address = decodeUtf8(e.packet.payload!);
// tcp:1234\0 // tcp:12345\0
const port = Number.parseInt(address.substring(4)); const port = Number.parseInt(address.substring(4));
if (this.localPortToHandler.has(port)) { if (this.localPortToHandler.has(port)) {
this.localPortToHandler.get(port)!.onSocket(e.packet, e.socket); this.localPortToHandler.get(port)!.onSocket(e.packet, e.socket);