feat(adb): group sub commands to separate objects

This commit is contained in:
Simon Chan 2020-09-27 17:10:12 +08:00
parent 7836343829
commit ba2be7172d
19 changed files with 453 additions and 167 deletions

View file

@ -10,6 +10,7 @@
"addrs",
"fluentui",
"getprop",
"killforward",
"lapo",
"lstat",
"mitm",

View file

@ -1,6 +1,8 @@
import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
import { EventEmitter } from '@yume-chan/event';
export * from './watcher';
export const WebUsbDeviceFilter: USBDeviceFilter = {
classCode: 0xFF,
subclassCode: 0x42,
@ -21,58 +23,15 @@ export function decodeUtf8(buffer: ArrayBuffer): string {
}
export default class AdbWebBackend implements AdbBackend {
public static async fromDevice(device: USBDevice): Promise<AdbWebBackend> {
await device.open();
for (const configuration of device.configurations) {
for (const interface_ of configuration.interfaces) {
for (const alternate of interface_.alternates) {
if (alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode &&
alternate.interfaceClass === WebUsbDeviceFilter.classCode &&
alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode) {
if (device.configuration?.configurationValue !== configuration.configurationValue) {
await device.selectConfiguration(configuration.configurationValue);
}
if (!interface_.claimed) {
await device.claimInterface(interface_.interfaceNumber);
}
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
await device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
}
let inEndpointNumber: number | undefined;
let outEndpointNumber: number | undefined;
for (const endpoint of alternate.endpoints) {
switch (endpoint.direction) {
case 'in':
inEndpointNumber = endpoint.endpointNumber;
if (outEndpointNumber !== undefined) {
return new AdbWebBackend(device, inEndpointNumber, outEndpointNumber);
}
break;
case 'out':
outEndpointNumber = endpoint.endpointNumber;
if (inEndpointNumber !== undefined) {
return new AdbWebBackend(device, inEndpointNumber, outEndpointNumber);
}
break;
}
}
}
}
}
}
throw new Error('Unknown error');
public static async getDevices(): Promise<AdbWebBackend[]> {
const devices = await window.navigator.usb.getDevices();
return devices.map(device => new AdbWebBackend(device));
}
public static async pickDevice(): Promise<AdbWebBackend | undefined> {
public static async requestDevice(): Promise<AdbWebBackend | undefined> {
try {
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
return AdbWebBackend.fromDevice(device);
return new AdbWebBackend(device);
} catch (e) {
switch (e.name) {
case 'NotFoundError':
@ -85,18 +44,72 @@ export default class AdbWebBackend implements AdbBackend {
private _device: USBDevice;
public get name() { return this._device.productName; }
public get serial(): string { return this._device.serialNumber!; }
private readonly onDisconnectedEvent = new EventEmitter<void>();
public readonly onDisconnected = this.onDisconnectedEvent.event;
public get name(): string { return this._device.productName!; }
private readonly disconnectEvent = new EventEmitter<void>();
public readonly onDisconnected = this.disconnectEvent.event;
private _inEndpointNumber!: number;
private _outEndpointNumber!: number;
private constructor(device: USBDevice, inEndPointNumber: number, outEndPointNumber: number) {
public constructor(device: USBDevice) {
this._device = device;
this._inEndpointNumber = inEndPointNumber;
this._outEndpointNumber = outEndPointNumber;
window.navigator.usb.addEventListener('disconnect', this.handleDisconnect);
}
private handleDisconnect = (e: USBConnectionEvent) => {
if (e.device === this._device) {
this.disconnectEvent.fire();
}
};
public async connect(): Promise<void> {
if (!this._device.opened) {
await this._device.open();
}
for (const configuration of this._device.configurations) {
for (const interface_ of configuration.interfaces) {
for (const alternate of interface_.alternates) {
if (alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode &&
alternate.interfaceClass === WebUsbDeviceFilter.classCode &&
alternate.interfaceSubclass === WebUsbDeviceFilter.subclassCode) {
if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
await this._device.selectConfiguration(configuration.configurationValue);
}
if (!interface_.claimed) {
await this._device.claimInterface(interface_.interfaceNumber);
}
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
}
for (const endpoint of alternate.endpoints) {
switch (endpoint.direction) {
case 'in':
this._inEndpointNumber = endpoint.endpointNumber;
if (this._outEndpointNumber !== undefined) {
return;
}
break;
case 'out':
this._outEndpointNumber = endpoint.endpointNumber;
if (this._inEndpointNumber !== undefined) {
return;
}
break;
}
}
}
}
}
}
throw new Error('Unknown error');
}
public *iterateKeys(): Generator<ArrayBuffer, void, void> {
@ -133,38 +146,23 @@ export default class AdbWebBackend implements AdbBackend {
}
public async write(buffer: ArrayBuffer): Promise<void> {
try {
await this._device.transferOut(this._outEndpointNumber, buffer);
} catch (e) {
if (e instanceof Error && e.name === 'NotFoundError') {
this.onDisconnectedEvent.fire();
}
throw e;
}
await this._device.transferOut(this._outEndpointNumber, buffer);
}
public async read(length: number): Promise<ArrayBuffer> {
try {
const result = await this._device.transferIn(this._inEndpointNumber, length);
const result = await this._device.transferIn(this._inEndpointNumber, length);
if (result.status === 'stall') {
await this._device.clearHalt('in', this._inEndpointNumber);
}
const { buffer } = result.data!;
return buffer;
} catch (e) {
if (e instanceof Error && e.name === 'NotFoundError') {
this.onDisconnectedEvent.fire();
}
throw e;
if (result.status === 'stall') {
await this._device.clearHalt('in', this._inEndpointNumber);
}
const { buffer } = result.data!;
return buffer;
}
public async dispose() {
this.onDisconnectedEvent.dispose();
window.navigator.usb.removeEventListener('disconnect', this.handleDisconnect);
this.disconnectEvent.dispose();
await this._device.close();
}
}

View file

@ -0,0 +1,15 @@
export class AdbWebBackendWatcher {
private callback: () => void;
public constructor(callback: () => void) {
this.callback = callback;
window.navigator.usb.addEventListener('connect', callback);
window.navigator.usb.addEventListener('disconnect', callback);
}
public dispose(): void {
window.navigator.usb.removeEventListener('connect', this.callback);
window.navigator.usb.removeEventListener('disconnect', this.callback);
}
}

View file

@ -2,6 +2,7 @@ import { PromiseResolver } from '@yume-chan/async-operation-manager';
import { DisposableList } from '@yume-chan/event';
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
import { AdbBackend } from './backend';
import { AdbReverseCommand, AdbTcpIpCommand } from './commands';
import { AdbFeatures } from './features';
import { FrameBuffer } from './framebuffer';
import { AdbCommand } from './packet';
@ -16,6 +17,8 @@ export enum AdbPropKey {
}
export class Adb {
private packetDispatcher: AdbPacketDispatcher;
private backend: AdbBackend;
public get onDisconnected() { return this.backend.onDisconnected; }
@ -36,29 +39,38 @@ export class Adb {
private _features: AdbFeatures[] | undefined;
public get features() { return this._features; }
private packetDispatcher: AdbPacketDispatcher;
public readonly tcpip: AdbTcpIpCommand;
public readonly reverse: AdbReverseCommand;
public constructor(backend: AdbBackend) {
this.backend = backend;
this.packetDispatcher = new AdbPacketDispatcher(backend);
this.tcpip = new AdbTcpIpCommand(this);
this.reverse = new AdbReverseCommand(this.packetDispatcher);
backend.onDisconnected(this.dispose, this);
}
public async connect(authenticators = AdbDefaultAuthenticators) {
await this.backend.connect?.();
this.packetDispatcher.start();
const version = 0x01000001;
const features = [
'shell_v2',
'cmd',
AdbFeatures.StatV2,
'shell_v2', // 9
'cmd', // 7
AdbFeatures.StatV2, // 5
'ls_v2',
'fixed_push_mkdir',
'apex',
'abb',
'fixed_push_symlink_timestamp',
'abb_exec',
'remount_shell',
'fixed_push_mkdir', // 4
'apex', // 2
'abb', // 8
'fixed_push_symlink_timestamp', // 1
'abb_exec', // 6
'remount_shell', // 3
'track_app',
'sendrecv_v2',
'sendrecv_v2_brotli',
@ -163,33 +175,6 @@ export class Adb {
}
}
public async getDaemonTcpAddresses(): Promise<string[]> {
const propAddr = (await this.shell('getprop', 'service.adb.listen_addrs')).trim();
if (propAddr) {
return propAddr.split(',');
}
let port = (await this.shell('getprop', 'service.adb.tcp.port')).trim();
if (port) {
return [`0.0.0.0:${port}`];
}
port = (await this.shell('getprop', 'persist.adb.tcp.port')).trim();
if (port) {
return [`0.0.0.0:${port}`];
}
return [];
}
public setDaemonTcpPort(port = 5555): Promise<string> {
return this.createStreamAndReadAll(`tcpip:${port}`);
}
public disableDaemonTcp(): Promise<string> {
return this.createStreamAndReadAll('usb:');
}
public async sync(): Promise<AdbSync> {
const stream = await this.createStream('sync:');
return new AdbSync(stream, this);
@ -198,11 +183,7 @@ export class Adb {
public async framebuffer(): Promise<FrameBuffer> {
const stream = await this.createStream('framebuffer:');
const buffered = new AdbBufferedStream(stream);
return await FrameBuffer.deserialize({
read: buffered.read.bind(buffered),
encodeUtf8: this.backend.encodeUtf8.bind(this.backend),
decodeUtf8: this.backend.decodeUtf8.bind(this.backend),
});
return FrameBuffer.deserialize(buffered);
}
public async createStream(service: string): Promise<AdbStream> {

View file

@ -3,10 +3,14 @@ import { Event } from '@yume-chan/event';
export type AdbKeyIterator = Iterator<ArrayBuffer> | AsyncIterator<ArrayBuffer>;
export interface AdbBackend {
readonly serial: string;
readonly name: string | undefined;
readonly onDisconnected: Event<void>;
connect?(): void | Promise<void>;
iterateKeys(): AdbKeyIterator;
generateKey(): ArrayBuffer | Promise<ArrayBuffer>;

View file

@ -0,0 +1,11 @@
import { AutoDisposable } from '@yume-chan/event';
import { Adb } from '../adb';
export class AdbCommandBase extends AutoDisposable {
protected adb: Adb;
public constructor(adb: Adb) {
super();
this.adb = adb;
}
}

View file

@ -0,0 +1,3 @@
export * from './base';
export * from './tcpip';
export * from './reverse';

View file

@ -0,0 +1,122 @@
import { AutoDisposable } from '@yume-chan/event';
import { Struct } from '@yume-chan/struct';
import { Adb } from '../adb';
import { AdbPacket } from '../packet';
import { AdbBufferedStream, AdbPacketDispatcher, AdbStream } from '../stream';
import { AdbCommandBase } from './base';
export interface AdbReverseHandler {
onStream(packet: AdbPacket, stream: AdbStream): void;
}
export interface AdbForwardListener {
deviceSerial: string;
localName: string;
remoteName: string;
}
const AdbReverseStringResponse =
new Struct({ littleEndian: true })
.string('length', { length: 4 })
.string('content', { lengthField: 'length' });
const AdbReverseErrorResponse =
AdbReverseStringResponse
.afterParsed((value) => {
throw new Error(value.content);
});
export class AdbReverseCommand extends AutoDisposable {
private portToHandlerMap = new Map<number, AdbReverseHandler>();
private devicePortToPortMap = new Map<number, number>();
private dispatcher: AdbPacketDispatcher;
private listening = false;
public constructor(dispatcher: AdbPacketDispatcher) {
super();
this.dispatcher = dispatcher;
}
public async list(): Promise<AdbForwardListener[]> {
const stream = await this.dispatcher.createStream('reverse:list-forward');
const buffered = new AdbBufferedStream(stream);
const response = await AdbReverseStringResponse.deserialize(buffered);
return response.content!.split('\n').map(line => {
const [deviceSerial, localName, remoteName] = line.split(' ');
return { deviceSerial, localName, remoteName };
});
}
public async add(
port: number,
handler: AdbReverseHandler,
devicePort: number = 0,
): Promise<number> {
if (!this.listening) {
this.addDisposable(this.dispatcher.onStream(e => {
if (e.handled) {
return;
}
const address = this.dispatcher.backend.decodeUtf8(e.packet.payload!);
const port = Number.parseInt(address.substring(4));
if (this.portToHandlerMap.has(port)) {
this.portToHandlerMap.get(port)!.onStream(e.packet, e.stream);
e.handled = true;
}
}));
}
const stream = await this.dispatcher.createStream(`reverse:forward:tcp:${devicePort};tcp:${port}`);
const buffered = new AdbBufferedStream(stream);
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
if (success) {
const response = await AdbReverseStringResponse.deserialize(buffered);
devicePort = Number.parseInt(response.content!, 10);
this.portToHandlerMap.set(port, handler);
this.devicePortToPortMap.set(devicePort, port);
return devicePort;
} else {
return await AdbReverseErrorResponse.deserialize(buffered);
}
}
public async remove(devicePort: number): Promise<void> {
const stream = await this.dispatcher.createStream(`reverse:killforward:tcp:${devicePort}`);
const buffered = new AdbBufferedStream(stream);
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
if (success) {
if (this.devicePortToPortMap.has(devicePort)) {
this.portToHandlerMap.delete(this.devicePortToPortMap.get(devicePort)!);
this.devicePortToPortMap.delete(devicePort);
}
} else {
await AdbReverseErrorResponse.deserialize(buffered);
}
}
public async removeAll(): Promise<void> {
const stream = await this.dispatcher.createStream(`reverse:killforward-all`);
const buffered = new AdbBufferedStream(stream);
const success = this.dispatcher.backend.decodeUtf8(await buffered.read(4)) === 'OKAY';
if (success) {
this.devicePortToPortMap.clear();
this.portToHandlerMap.clear();
} else {
await AdbReverseErrorResponse.deserialize(buffered);
}
}
}

View file

@ -0,0 +1,45 @@
import { AdbCommandBase } from './base';
export class AdbTcpIpCommand extends AdbCommandBase {
private async getProp(key: string): Promise<string> {
const output = await this.adb.shell('getprop', key);
return output.trim();
}
public async getAddresses(): Promise<string[]> {
const propAddr = await this.getProp('service.adb.listen_addrs');
if (propAddr) {
return propAddr.split(',');
}
let port = await this.getProp('service.adb.tcp.port');
if (port) {
return [`0.0.0.0:${port}`];
}
port = await this.getProp('persist.adb.tcp.port');
if (port) {
return [`0.0.0.0:${port}`];
}
return [];
}
public async setPort(port: number): Promise<void> {
if (port <= 0) {
throw new Error(`Invalid port ${port}`);
}
const output = await this.adb.createStreamAndReadAll(`tcpip:${port}`);
if (output !== `restarting in TCP mode port: ${port}\n`) {
throw new Error('Invalid response');
}
}
public async disable(): Promise<void> {
const output = await this.adb.createStreamAndReadAll('usb:');
if (output !== 'restarting in USB mode\n') {
throw new Error('Invalid response');
}
}
}

View file

@ -1,3 +1,4 @@
import { StructDeserializationContext } from '@yume-chan/struct';
import { AdbStreamBase } from './controller';
import { AdbReadableStream } from './readable-stream';
import { AdbStream } from './stream';
@ -60,7 +61,9 @@ export class BufferedStream<T extends Stream> {
}
}
export class AdbBufferedStream extends BufferedStream<AdbReadableStream> implements AdbStreamBase {
export class AdbBufferedStream
extends BufferedStream<AdbReadableStream>
implements AdbStreamBase, StructDeserializationContext {
public get backend() { return this.stream.backend; }
public get localId() { return this.stream.localId; }
@ -74,4 +77,12 @@ export class AdbBufferedStream extends BufferedStream<AdbReadableStream> impleme
public write(data: ArrayBuffer): Promise<void> {
return this.stream.write(data);
}
public decodeUtf8(buffer: ArrayBuffer): string {
return this.backend.decodeUtf8(buffer);
}
public encodeUtf8(input: string): ArrayBuffer {
return this.backend.encodeUtf8(input);
}
}

View file

@ -36,14 +36,13 @@ export class AdbPacketDispatcher extends AutoDisposable {
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
public get onError() { return this.errorEvent.event; }
private _running = true;
private _running = false;
public get running() { return this._running; }
public constructor(backend: AdbBackend) {
super();
this.backend = backend;
this.receiveLoop();
}
private async receiveLoop() {
@ -129,7 +128,8 @@ export class AdbPacketDispatcher extends AutoDisposable {
const [localId] = this.initializers.add<number>();
this.initializers.resolve(localId, undefined);
const controller = new AdbStreamController(localId, packet.arg0, this);
const remoteId = packet.arg0;
const controller = new AdbStreamController(localId, remoteId, this);
const stream = new AdbStream(controller);
const args: AdbStreamCreatedEventArgs = {
@ -141,12 +141,17 @@ export class AdbPacketDispatcher extends AutoDisposable {
if (args.handled) {
this.streams.set(localId, controller);
await this.sendPacket(AdbCommand.OK, localId, packet.arg0);
await this.sendPacket(AdbCommand.OK, localId, remoteId);
} else {
await this.sendPacket(AdbCommand.Close, 0, packet.arg0);
await this.sendPacket(AdbCommand.Close, 0, remoteId);
}
}
public start() {
this._running = true;
this.receiveLoop();
}
public async createStream(service: string): Promise<AdbStream> {
const [localId, initializer] = this.initializers.add<number>();
await this.sendPacket(AdbCommand.Open, localId, 0, service);

View file

@ -1,10 +1,12 @@
import { DefaultButton, Dialog, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
import { Adb } from '@yume-chan/adb';
import AdbWebBackend from '@yume-chan/adb-backend-web';
import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, TooltipHost } from '@fluentui/react';
import { Adb, AdbBackend } from '@yume-chan/adb';
import AdbWebBackend, { AdbWebBackendWatcher } from '@yume-chan/adb-backend-web';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ErrorDialogContext } from './error-dialog';
import { withDisplayName } from './utils';
const DropdownStyles = { dropdown: { width: 300 } };
interface ConnectProps {
device: Adb | undefined;
@ -17,12 +19,66 @@ export default withDisplayName('Connect', ({
}: ConnectProps): JSX.Element | null => {
const { show: showErrorDialog } = useContext(ErrorDialogContext);
const [backendOptions, setBackendOptions] = useState<IDropdownOption[]>([]);
const [selectedBackend, setSelectedBackend] = useState<AdbBackend | undefined>();
useEffect(() => {
async function refresh() {
const backendList = await AdbWebBackend.getDevices();
const options = backendList.map(item => ({
key: item.serial,
text: `${item.serial} ${item.name ? `(${item.name})` : ''}`,
data: item,
}));
setBackendOptions(options);
setSelectedBackend(old => {
if (old && backendList.some(item => item.serial === old.serial)) {
return old;
}
return backendList[0];
});
};
refresh();
const watcher = new AdbWebBackendWatcher(refresh);
return () => watcher.dispose();
}, []);
const handleSelectedBackendChange = (
_e: React.FormEvent<HTMLDivElement>,
option?: IDropdownOption,
) => {
setSelectedBackend(option?.data as AdbBackend);
};
const requestAccess = useCallback(async () => {
const backend = await AdbWebBackend.requestDevice();
if (backend) {
setBackendOptions(list => {
for (const item of list) {
if (item.key === backend.serial) {
setSelectedBackend(item.data);
return list;
}
}
setSelectedBackend(backend);
return [...list, {
key: backend.serial,
text: `${backend.serial} ${backend.name ? `(${backend.name})` : ''}`,
data: backend,
}];
});
}
}, []);
const [connecting, setConnecting] = useState(false);
const connect = useCallback(async () => {
try {
const backend = await AdbWebBackend.pickDevice();
if (backend) {
const device = new Adb(backend);
if (selectedBackend) {
const device = new Adb(selectedBackend);
try {
setConnecting(true);
await device.connect();
@ -37,7 +93,7 @@ export default withDisplayName('Connect', ({
} finally {
setConnecting(false);
}
}, [onDeviceChange]);
}, [selectedBackend, onDeviceChange]);
const disconnect = useCallback(async () => {
try {
await device!.dispose();
@ -53,18 +109,42 @@ export default withDisplayName('Connect', ({
}, [device, onDeviceChange]);
return (
<>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8, padding: 8 }}>
{!device && <StackItem>
<PrimaryButton text="Connect" onClick={connect} />
</StackItem>}
{device && <StackItem>
<Stack
horizontal
verticalAlign="end"
tokens={{ childrenGap: 8, padding: 8 }}
>
<Dropdown
disabled={!!device || backendOptions.length === 0}
label="Available devices"
placeholder="No available devices"
options={backendOptions}
styles={DropdownStyles}
selectedKey={selectedBackend?.serial}
onChange={handleSelectedBackendChange}
/>
{!device ? (
<>
<PrimaryButton
text="Connect"
disabled={!selectedBackend}
primary={!!selectedBackend}
onClick={connect}
/>
<TooltipHost
content="WebADB can't connect to anything without your explicit permission."
>
<DefaultButton
text="Add new device"
primary={!selectedBackend}
onClick={requestAccess}
/>
</TooltipHost>
</>
) : (
<DefaultButton text="Disconnect" onClick={disconnect} />
</StackItem>}
<StackItem>
{device && `Connected to ${device.name}`}
</StackItem>
</Stack>
)}
<Dialog
hidden={!connecting}
@ -75,6 +155,6 @@ export default withDisplayName('Connect', ({
>
<ProgressIndicator />
</Dialog>
</>
</Stack>
);
});

View file

@ -12,7 +12,8 @@ import FileManager from './routes/file-manager';
import FrameBuffer from './routes/framebuffer';
import Intro from './routes/intro';
import Shell from './routes/shell';
import TcpIp from './routes/tcp-ip';
import TcpIp from './routes/tcpip';
import { CommonStackTokens } from './styles';
initializeIcons();
@ -105,7 +106,7 @@ function App(): JSX.Element | null {
<Separator />
</StackItem>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<Stack horizontal verticalFill tokens={{ childrenGap: 8 }}>
<Stack horizontal verticalFill tokens={CommonStackTokens}>
<StackItem>
<Nav
styles={{ root: { width: 250 } }}

View file

@ -9,6 +9,8 @@ const classNames = mergeStyleSets({
},
});
const BoldTextStyles = { root: { fontWeight: '600' } };
interface CopyLinkProps {
href: string;
}
@ -56,7 +58,7 @@ export default withDisplayName('Intro', () => {
The latest version of Google Chrome (or Microsoft Edge) is recommended for best compatibility.
</Text>
<Text block styles={{ root: { fontWeight: '600' } }}>
<Text block styles={BoldTextStyles}>
Windows user?
</Text>
<Text block>
@ -65,7 +67,7 @@ export default withDisplayName('Intro', () => {
.
</Text>
<Text block styles={{ root: { fontWeight: '600' } }}>
<Text block styles={BoldTextStyles}>
Got "Unable to claim interface" error?
</Text>
<Text block>
@ -73,7 +75,7 @@ export default withDisplayName('Intro', () => {
1. Make sure ADB server is not running (run `adb kill-server` to stop it).<br />
2. Make sure no other Android management tools are running
</Text>
<Text block styles={{ root: { fontWeight: '600' } }}>
<Text block styles={BoldTextStyles}>
Got "Access denied" error?
</Text>
<Text block>
@ -82,7 +84,7 @@ export default withDisplayName('Intro', () => {
https://bugs.chromium.org/p/chromium/issues/detail?id=1127206
</Link>
</Text>
<Text block styles={{ root: { fontWeight: '600' } }}>
<Text block styles={BoldTextStyles}>
Can I connect my device wirelessly (ADB over WiFi)?
</Text>
<Text block>

View file

@ -8,11 +8,14 @@ import 'xterm/css/xterm.css';
import { ResizeObserver, withDisplayName } from '../utils';
import { RouteProps } from './type';
const containerStyle: CSSProperties = {
const ResizeObserverStyle: CSSProperties = {
width: '100%',
height: '100%',
};
const UpIconProps = { iconName: 'ChevronUp' };
const DownIconProps = { iconName: 'ChevronDown' };
export default withDisplayName('Shell', ({
device,
}: RouteProps): JSX.Element | null => {
@ -99,21 +102,21 @@ export default withDisplayName('Shell', ({
<StackItem>
<IconButton
disabled={!findKeyword}
iconProps={{ iconName: 'ChevronUp' }}
iconProps={UpIconProps}
onClick={findPrevious}
/>
</StackItem>
<StackItem>
<IconButton
disabled={!findKeyword}
iconProps={{ iconName: 'ChevronDown' }}
iconProps={DownIconProps}
onClick={findNext}
/>
</StackItem>
</Stack>
</StackItem>
<StackItem grow styles={{ root: { minHeight: 0 } }}>
<ResizeObserver style={containerStyle} onResize={handleResize}>
<ResizeObserver style={ResizeObserverStyle} onResize={handleResize}>
<div ref={handleContainerRef} style={{ height: '100%' }} />
</ResizeObserver>
</StackItem>

View file

@ -1,6 +1,7 @@
import { Label, MessageBar, PrimaryButton, Stack, StackItem, Text, TextField } from '@fluentui/react';
import { useId } from '@uifabric/react-hooks';
import React, { useCallback, useEffect, useState } from 'react';
import { CommonStackTokens } from '../styles';
import { withDisplayName } from '../utils';
import { RouteProps } from './type';
@ -19,7 +20,7 @@ export default withDisplayName('TcpIp', ({
return;
}
const result = await device.getDaemonTcpAddresses();
const result = await device.tcpip.getAddresses();
setTcpAddresses(result);
}, [device]);
@ -30,8 +31,7 @@ export default withDisplayName('TcpIp', ({
return;
}
const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10));
console.log(result);
await device.tcpip.setPort(Number.parseInt(tcpPortValue, 10));
}, [device, tcpPortValue]);
const disableTcp = useCallback(async () => {
@ -39,8 +39,7 @@ export default withDisplayName('TcpIp', ({
return;
}
const result = await device.disableDaemonTcp();
console.log(result);
await device.tcpip.disable();
}, [device]);
return (
@ -55,7 +54,7 @@ export default withDisplayName('TcpIp', ({
<Text>Your device will disconnect after changing ADB over WiFi config.</Text>
</MessageBar>
</StackItem>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
<StackItem>
<PrimaryButton text="Update Status" disabled={!device} onClick={queryTcpAddress} />
</StackItem>
@ -66,7 +65,7 @@ export default withDisplayName('TcpIp', ({
: 'Disabled')}
</StackItem>
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 8 }}>
<Stack horizontal verticalAlign="center" tokens={CommonStackTokens}>
<StackItem>
<Label htmlFor={tcpPortInputId}>Port: </Label>
</StackItem>

View file

@ -0,0 +1 @@
export const CommonStackTokens = { childrenGap: 8 };

View file

@ -34,7 +34,7 @@ export namespace VariableLengthArray {
export interface Options<
TInit = object,
TLengthField extends KeyOfType<TInit, number> = any,
TLengthField extends KeyOfType<TInit, number | string> = any,
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> extends FieldDescriptorBaseOptions {
lengthField: TLengthField;
@ -131,7 +131,7 @@ export interface VariableLengthArray<
TName extends string = string,
TType extends Array.SubType = Array.SubType,
TInit = object,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number> = any,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string> = any,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>,
TOptions extends VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>
@ -156,7 +156,11 @@ registerFieldTypeDefinition(
async deserialize(
{ context, field, object }
): Promise<{ value: string | ArrayBuffer | undefined, extra?: ArrayBuffer; }> {
const length = object[field.options.lengthField];
let length = object[field.options.lengthField];
if (typeof length === 'string') {
length = Number.parseInt(length, 10);
}
if (length === 0) {
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
switch (field.subType) {

View file

@ -39,7 +39,7 @@ interface AddArrayFieldDescriptor<
<
TName extends string,
TType extends Array.SubType,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(
@ -91,7 +91,7 @@ interface AddArraySubTypeFieldDescriptor<
<
TName extends string,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number>,
TLengthField extends VariableLengthArray.KeyOfType<TInit, number | string>,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior,
TTypeScriptType = VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(