Decouple android device management from Flipper core/store

Summary: See earlier diffs in the stack. This diff decouple android device management from the Redux store, replacing it with specific events.

Reviewed By: timur-valiev

Differential Revision: D30286345

fbshipit-source-id: 42f52056bf123b862e2fc087f2e7130c02bdd742
This commit is contained in:
Michel Weststrate
2021-08-17 04:43:18 -07:00
committed by Facebook GitHub Bot
parent bf65da0e72
commit 03f2f95a31
7 changed files with 293 additions and 246 deletions

View File

@@ -2,7 +2,6 @@
exports[`can create a Fake flipper with legacy wrapper 1`] = ` exports[`can create a Fake flipper with legacy wrapper 1`] = `
Object { Object {
"androidEmulators": Array [],
"clients": Array [ "clients": Array [
Object { Object {
"id": "TestApp#Android#MockAndroidDevice#serial", "id": "TestApp#Android#MockAndroidDevice#serial",
@@ -36,6 +35,7 @@ Object {
"TestPlugin", "TestPlugin",
], ],
}, },
"flipperServer": undefined,
"selectedApp": "TestApp#Android#MockAndroidDevice#serial", "selectedApp": "TestApp#Android#MockAndroidDevice#serial",
"selectedAppPluginListRevision": 0, "selectedAppPluginListRevision": 0,
"selectedDevice": Object { "selectedDevice": Object {

View File

@@ -16,15 +16,30 @@ import Client from '../Client';
import {notification} from 'antd'; import {notification} from 'antd';
export default async (store: Store, logger: Logger) => { export default async (store: Store, logger: Logger) => {
const {enableAndroid} = store.getState().settingsState; const {enableAndroid, androidHome} = store.getState().settingsState;
const server = await startFlipperServer( const server = await startFlipperServer(
{ {
enableAndroid, enableAndroid,
androidHome,
serverPorts: store.getState().application.serverPorts,
}, },
store, store,
logger, logger,
); );
store.dispatch({
type: 'SET_FLIPPER_SERVER',
payload: server,
});
server.on('notification', (notif) => {
notification.open({
message: notif.title,
description: notif.description,
type: notif.type,
});
});
server.on('server-start-error', (err) => { server.on('server-start-error', (err) => {
notification.error({ notification.error({
message: 'Failed to start connection server', message: 'Failed to start connection server',
@@ -50,6 +65,12 @@ export default async (store: Store, logger: Logger) => {
}); });
server.on('device-connected', (device) => { server.on('device-connected', (device) => {
logger.track('usage', 'register-device', {
os: 'Android',
name: device.title,
serial: device.serial,
});
device.loadDevicePlugins( device.loadDevicePlugins(
store.getState().plugins.devicePlugins, store.getState().plugins.devicePlugins,
store.getState().connections.enabledDevicePlugins, store.getState().connections.enabledDevicePlugins,
@@ -61,6 +82,14 @@ export default async (store: Store, logger: Logger) => {
}); });
}); });
server.on('device-disconnected', (device) => {
logger.track('usage', 'unregister-device', {
os: device.os,
serial: device.serial,
});
// N.B.: note that we don't remove the device, we keep it in offline mode!
});
server.on('client-connected', (payload) => server.on('client-connected', (payload) =>
handleClientConnected(store, payload), handleClientConnected(store, payload),
); );

View File

@@ -25,6 +25,7 @@ import {deconstructClientId} from '../utils/clientUtils';
import type {RegisterPluginAction} from './plugins'; import type {RegisterPluginAction} from './plugins';
import MetroDevice from '../server/devices/metro/MetroDevice'; import MetroDevice from '../server/devices/metro/MetroDevice';
import {Logger} from 'flipper-plugin'; import {Logger} from 'flipper-plugin';
import {FlipperServer} from '../server/FlipperServer';
export type StaticViewProps = {logger: Logger}; export type StaticViewProps = {logger: Logger};
@@ -63,7 +64,6 @@ export const persistMigrations = {
type StateV2 = { type StateV2 = {
devices: Array<BaseDevice>; devices: Array<BaseDevice>;
androidEmulators: Array<string>;
selectedDevice: null | BaseDevice; selectedDevice: null | BaseDevice;
selectedPlugin: null | string; selectedPlugin: null | string;
selectedApp: null | string; selectedApp: null | string;
@@ -81,6 +81,7 @@ type StateV2 = {
deepLinkPayload: unknown; deepLinkPayload: unknown;
staticView: StaticView; staticView: StaticView;
selectedAppPluginListRevision: number; selectedAppPluginListRevision: number;
flipperServer: FlipperServer | undefined;
}; };
type StateV1 = Omit<StateV2, 'enabledPlugins' | 'enabledDevicePlugins'> & { type StateV1 = Omit<StateV2, 'enabledPlugins' | 'enabledDevicePlugins'> & {
@@ -102,10 +103,6 @@ export type Action =
type: 'REGISTER_DEVICE'; type: 'REGISTER_DEVICE';
payload: BaseDevice; payload: BaseDevice;
} }
| {
type: 'REGISTER_ANDROID_EMULATORS';
payload: Array<string>;
}
| { | {
type: 'SELECT_DEVICE'; type: 'SELECT_DEVICE';
payload: BaseDevice; payload: BaseDevice;
@@ -182,13 +179,16 @@ export type Action =
| { | {
type: 'APP_PLUGIN_LIST_CHANGED'; type: 'APP_PLUGIN_LIST_CHANGED';
} }
| {
type: 'SET_FLIPPER_SERVER';
payload: FlipperServer;
}
| RegisterPluginAction; | RegisterPluginAction;
const DEFAULT_PLUGIN = 'DeviceLogs'; const DEFAULT_PLUGIN = 'DeviceLogs';
const DEFAULT_DEVICE_BLACKLIST = [MacDevice, MetroDevice]; const DEFAULT_DEVICE_BLACKLIST = [MacDevice, MetroDevice];
const INITAL_STATE: State = { const INITAL_STATE: State = {
devices: [], devices: [],
androidEmulators: [],
selectedDevice: null, selectedDevice: null,
selectedApp: null, selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN, selectedPlugin: DEFAULT_PLUGIN,
@@ -208,10 +208,18 @@ const INITAL_STATE: State = {
deepLinkPayload: null, deepLinkPayload: null,
staticView: WelcomeScreenStaticView, staticView: WelcomeScreenStaticView,
selectedAppPluginListRevision: 0, selectedAppPluginListRevision: 0,
flipperServer: undefined,
}; };
export default (state: State = INITAL_STATE, action: Actions): State => { export default (state: State = INITAL_STATE, action: Actions): State => {
switch (action.type) { switch (action.type) {
case 'SET_FLIPPER_SERVER': {
return {
...state,
flipperServer: action.payload,
};
}
case 'SET_STATIC_VIEW': { case 'SET_STATIC_VIEW': {
const {payload, deepLinkPayload} = action; const {payload, deepLinkPayload} = action;
const {selectedPlugin} = state; const {selectedPlugin} = state;
@@ -241,13 +249,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
: state.userPreferredDevice, : state.userPreferredDevice,
}); });
} }
case 'REGISTER_ANDROID_EMULATORS': {
const {payload} = action;
return {
...state,
androidEmulators: payload,
};
}
case 'REGISTER_DEVICE': { case 'REGISTER_DEVICE': {
const {payload} = action; const {payload} = action;
@@ -256,9 +258,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
(device) => device.serial === payload.serial, (device) => device.serial === payload.serial,
); );
if (existing !== -1) { if (existing !== -1) {
console.warn( newDevices[existing].destroy();
`Got a new device instance for already existing serial ${payload.serial}`,
);
newDevices[existing] = payload; newDevices[existing] = payload;
} else { } else {
newDevices.push(payload); newDevices.push(payload);
@@ -270,6 +270,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
}); });
} }
// TODO: remove
case 'UNREGISTER_DEVICES': { case 'UNREGISTER_DEVICES': {
const deviceSerials = action.payload; const deviceSerials = action.payload;
@@ -290,6 +291,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
}), }),
); );
} }
case 'SELECT_PLUGIN': { case 'SELECT_PLUGIN': {
const {payload} = action; const {payload} = action;
const {selectedPlugin, selectedApp, deepLinkPayload} = payload; const {selectedPlugin, selectedApp, deepLinkPayload} = payload;
@@ -687,6 +689,7 @@ export function isPluginEnabled(
return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1; return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1;
} }
// TODO: remove!
export function destroyDevice(store: Store, logger: Logger, serial: string) { export function destroyDevice(store: Store, logger: Logger, serial: string) {
const device = store const device = store
.getState() .getState()

View File

@@ -33,31 +33,41 @@ const COLD_BOOT = 'cold-boot';
export function showEmulatorLauncher(store: Store) { export function showEmulatorLauncher(store: Store) {
renderReactRoot((unmount) => ( renderReactRoot((unmount) => (
<Provider store={store}> <Provider store={store}>
<LaunchEmulatorDialog <LaunchEmulatorContainer onClose={unmount} />
onClose={unmount}
getSimulators={getSimulators.bind(store)}
/>
</Provider> </Provider>
)); ));
} }
function LaunchEmulatorContainer({onClose}: {onClose: () => void}) {
const store = useStore();
const flipperServer = useStore((state) => state.connections.flipperServer);
return (
<LaunchEmulatorDialog
onClose={onClose}
getSimulators={getSimulators.bind(store)}
getEmulators={() => flipperServer!.android.getAndroidEmulators()}
/>
);
}
type GetSimulators = typeof getSimulators; type GetSimulators = typeof getSimulators;
export const LaunchEmulatorDialog = withTrackingScope( export const LaunchEmulatorDialog = withTrackingScope(
function LaunchEmulatorDialog({ function LaunchEmulatorDialog({
onClose, onClose,
getSimulators, getSimulators,
getEmulators,
}: { }: {
onClose: () => void; onClose: () => void;
getSimulators: GetSimulators; getSimulators: GetSimulators;
getEmulators: () => Promise<string[]>;
}) { }) {
const iosEnabled = useStore((state) => state.settingsState.enableIOS); const iosEnabled = useStore((state) => state.settingsState.enableIOS);
const androidEmulators = useStore((state) => const androidEnabled = useStore(
state.settingsState.enableAndroid (state) => state.settingsState.enableAndroid,
? state.connections.androidEmulators
: [],
); );
const [iosEmulators, setIosEmulators] = useState<IOSDeviceParams[]>([]); const [iosEmulators, setIosEmulators] = useState<IOSDeviceParams[]>([]);
const [androidEmulators, setAndroidEmulators] = useState<string[]>([]);
const store = useStore(); const store = useStore();
useEffect(() => { useEffect(() => {
@@ -75,6 +85,19 @@ export const LaunchEmulatorDialog = withTrackingScope(
}); });
}, [iosEnabled, getSimulators, store]); }, [iosEnabled, getSimulators, store]);
useEffect(() => {
if (!androidEnabled) {
return;
}
getEmulators()
.then((emulators) => {
setAndroidEmulators(emulators);
})
.catch((e) => {
console.warn('Failed to find emulatiors', e);
});
}, [androidEnabled, getEmulators]);
const items = [ const items = [
...(androidEmulators.length > 0 ...(androidEmulators.length > 0
? [<AndroidOutlined key="android logo" />] ? [<AndroidOutlined key="android logo" />]
@@ -82,11 +105,11 @@ export const LaunchEmulatorDialog = withTrackingScope(
...androidEmulators.map((name) => { ...androidEmulators.map((name) => {
const launch = (coldBoot: boolean) => { const launch = (coldBoot: boolean) => {
launchEmulator(name, coldBoot) launchEmulator(name, coldBoot)
.then(onClose)
.catch((e) => { .catch((e) => {
console.error(e); console.error('Failed to start emulator: ', e);
message.error('Failed to start emulator: ' + e); message.error('Failed to start emulator: ' + e);
}) });
.then(onClose);
}; };
const menu = ( const menu = (
<Menu <Menu

View File

@@ -14,16 +14,14 @@ import {createStore} from 'redux';
import {LaunchEmulatorDialog} from '../LaunchEmulator'; import {LaunchEmulatorDialog} from '../LaunchEmulator';
import {createRootReducer} from '../../../reducers'; import {createRootReducer} from '../../../reducers';
import {act} from 'react-dom/test-utils';
import {sleep} from 'flipper-plugin'; import {sleep} from 'flipper-plugin';
import {launchEmulator} from '../../../server/devices/android/AndroidDevice';
jest.mock('../../../server/devices/android/AndroidDevice', () => ({ jest.mock('../../../server/devices/android/AndroidDevice', () => ({
launchEmulator: jest.fn(() => Promise.resolve([])), launchEmulator: jest.fn(() => Promise.resolve([])),
})); }));
import {launchEmulator} from '../../../server/devices/android/AndroidDevice'; test('Can render and launch android apps - empty', async () => {
test('Can render and launch android apps', async () => {
const store = createStore(createRootReducer()); const store = createStore(createRootReducer());
const onClose = jest.fn(); const onClose = jest.fn();
@@ -32,6 +30,7 @@ test('Can render and launch android apps', async () => {
<LaunchEmulatorDialog <LaunchEmulatorDialog
onClose={onClose} onClose={onClose}
getSimulators={() => Promise.resolve([])} getSimulators={() => Promise.resolve([])}
getEmulators={() => Promise.resolve([])}
/> />
</Provider>, </Provider>,
); );
@@ -43,13 +42,32 @@ test('Can render and launch android apps', async () => {
No emulators available No emulators available
</div> </div>
`); `);
});
act(() => { test('Can render and launch android apps', async () => {
const store = createStore(createRootReducer());
store.dispatch({ store.dispatch({
type: 'REGISTER_ANDROID_EMULATORS', type: 'UPDATE_SETTINGS',
payload: ['emulator1', 'emulator2'], payload: {
}); ...store.getState().settingsState,
enableAndroid: true,
},
}); });
const onClose = jest.fn();
let p: Promise<any> | undefined = undefined;
const renderer = render(
<Provider store={store}>
<LaunchEmulatorDialog
onClose={onClose}
getSimulators={() => Promise.resolve([])}
getEmulators={() => (p = Promise.resolve(['emulator1', 'emulator2']))}
/>
</Provider>,
);
await p!;
expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(` expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
Array [ Array [

View File

@@ -18,21 +18,33 @@ import {CertificateExchangeMedium} from '../server/utils/CertificateProvider';
import {isLoggedIn} from '../fb-stubs/user'; import {isLoggedIn} from '../fb-stubs/user';
import React from 'react'; import React from 'react';
import {Typography} from 'antd'; import {Typography} from 'antd';
import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application'; import {
import androidDevice from './devices/android/androidDeviceManager'; ACTIVE_SHEET_SIGN_IN,
ServerPorts,
setActiveSheet,
} from '../reducers/application';
import {AndroidDeviceManager} from './devices/android/androidDeviceManager';
import iOSDevice from './devices/ios/iOSDeviceManager'; import iOSDevice from './devices/ios/iOSDeviceManager';
import metroDevice from './devices/metro/metroDeviceManager'; import metroDevice from './devices/metro/metroDeviceManager';
import desktopDevice from './devices/desktop/desktopDeviceManager'; import desktopDevice from './devices/desktop/desktopDeviceManager';
import BaseDevice from './devices/BaseDevice'; import BaseDevice from './devices/BaseDevice';
type FlipperServerEvents = { type FlipperServerEvents = {
'device-connected': BaseDevice;
'client-connected': Client;
'server-start-error': any; 'server-start-error': any;
notification: {
type: 'error';
title: string;
description: string;
};
'device-connected': BaseDevice;
'device-disconnected': BaseDevice;
'client-connected': Client;
}; };
export interface FlipperServerConfig { export interface FlipperServerConfig {
enableAndroid: boolean; enableAndroid: boolean;
androidHome: string;
serverPorts: ServerPorts;
} }
export async function startFlipperServer( export async function startFlipperServer(
@@ -59,6 +71,8 @@ export class FlipperServer {
readonly server: ServerController; readonly server: ServerController;
readonly disposers: ((() => void) | void)[] = []; readonly disposers: ((() => void) | void)[] = [];
android: AndroidDeviceManager;
// TODO: remove store argument // TODO: remove store argument
constructor( constructor(
public config: FlipperServerConfig, public config: FlipperServerConfig,
@@ -67,6 +81,7 @@ export class FlipperServer {
public logger: Logger, public logger: Logger,
) { ) {
this.server = new ServerController(logger, store); this.server = new ServerController(logger, store);
this.android = new AndroidDeviceManager(this);
} }
/** @private */ /** @private */
@@ -160,10 +175,8 @@ export class FlipperServer {
} }
async startDeviceListeners() { async startDeviceListeners() {
if (this.config.enableAndroid) {
this.disposers.push(await androidDevice(this.store, this.logger));
}
this.disposers.push( this.disposers.push(
await this.android.watchAndroidDevices(),
iOSDevice(this.store, this.logger), iOSDevice(this.store, this.logger),
metroDevice(this.store, this.logger), metroDevice(this.store, this.logger),
desktopDevice(this), desktopDevice(this),

View File

@@ -10,40 +10,41 @@
import AndroidDevice from './AndroidDevice'; import AndroidDevice from './AndroidDevice';
import KaiOSDevice from './KaiOSDevice'; import KaiOSDevice from './KaiOSDevice';
import child_process from 'child_process'; import child_process from 'child_process';
import {Store} from '../../../reducers/index';
import BaseDevice from '../BaseDevice'; import BaseDevice from '../BaseDevice';
import {Logger} from '../../../fb-interfaces/Logger';
import {getAdbClient} from './adbClient'; import {getAdbClient} from './adbClient';
import which from 'which'; import which from 'which';
import {promisify} from 'util'; import {promisify} from 'util';
import {ServerPorts} from '../../../reducers/application';
import {Client as ADBClient} from 'adbkit'; import {Client as ADBClient} from 'adbkit';
import {addErrorNotification} from '../../../reducers/notifications';
import {destroyDevice} from '../../../reducers/connections';
import {join} from 'path'; import {join} from 'path';
import {FlipperServer} from '../../FlipperServer';
import {notNull} from '../../utils/typeUtils';
function createDevice( export class AndroidDeviceManager {
// cache emulator path
private emulatorPath: string | undefined;
private devices: Map<string, AndroidDevice> = new Map();
constructor(public flipperServer: FlipperServer) {}
createDevice(
adbClient: ADBClient, adbClient: ADBClient,
device: any, device: any,
store: Store,
ports?: ServerPorts,
): Promise<AndroidDevice | undefined> { ): Promise<AndroidDevice | undefined> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
const type = const type =
device.type !== 'device' || device.id.startsWith('emulator') device.type !== 'device' || device.id.startsWith('emulator')
? 'emulator' ? 'emulator'
: 'physical'; : 'physical';
adbClient try {
.getProperties(device.id) const props = await adbClient.getProperties(device.id);
.then(async (props) => {
try { try {
let name = props['ro.product.model']; let name = props['ro.product.model'];
const abiString = props['ro.product.cpu.abilist'] || ''; const abiString = props['ro.product.cpu.abilist'] || '';
const sdkVersion = props['ro.build.version.sdk'] || ''; const sdkVersion = props['ro.build.version.sdk'] || '';
const abiList = abiString.length > 0 ? abiString.split(',') : []; const abiList = abiString.length > 0 ? abiString.split(',') : [];
if (type === 'emulator') { if (type === 'emulator') {
name = (await getRunningEmulatorName(device.id)) || name; name = (await this.getRunningEmulatorName(device.id)) || name;
} }
const isKaiOSDevice = Object.keys(props).some( const isKaiOSDevice = Object.keys(props).some(
(name) => name.startsWith('kaios') || name.startsWith('ro.kaios'), (name) => name.startsWith('kaios') || name.startsWith('ro.kaios'),
@@ -51,9 +52,12 @@ function createDevice(
const androidLikeDevice = new ( const androidLikeDevice = new (
isKaiOSDevice ? KaiOSDevice : AndroidDevice isKaiOSDevice ? KaiOSDevice : AndroidDevice
)(device.id, type, name, adbClient, abiList, sdkVersion); )(device.id, type, name, adbClient, abiList, sdkVersion);
if (ports) { if (this.flipperServer.config.serverPorts) {
await androidLikeDevice await androidLikeDevice
.reverse([ports.secure, ports.insecure]) .reverse([
this.flipperServer.config.serverPorts.secure,
this.flipperServer.config.serverPorts.insecure,
])
// We may not be able to establish a reverse connection, e.g. for old Android SDKs. // We may not be able to establish a reverse connection, e.g. for old Android SDKs.
// This is *generally* fine, because we hard-code the ports on the SDK side. // This is *generally* fine, because we hard-code the ports on the SDK side.
.catch((e) => { .catch((e) => {
@@ -75,8 +79,7 @@ function createDevice(
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
}) } catch (e) {
.catch((e) => {
if ( if (
e && e &&
e.message && e.message &&
@@ -87,35 +90,71 @@ function createDevice(
const isAuthorizationError = (e?.message as string)?.includes( const isAuthorizationError = (e?.message as string)?.includes(
'device unauthorized', 'device unauthorized',
); );
store.dispatch( if (!isAuthorizationError) {
addErrorNotification( console.error('Failed to connect to android device', e);
'Could not connect to ' + device.id, }
isAuthorizationError this.flipperServer.emit('notification', {
type: 'error',
title: 'Could not connect to ' + device.id,
description: isAuthorizationError
? 'Make sure to authorize debugging on the phone' ? 'Make sure to authorize debugging on the phone'
: 'Failed to setup connection', : 'Failed to setup connection: ' + e,
e, });
),
);
} }
resolve(undefined); // not ready yet, we will find it in the next tick resolve(undefined); // not ready yet, we will find it in the next tick
}); }
}); });
} }
export async function getActiveAndroidDevices( async getActiveAndroidDevices(): Promise<Array<BaseDevice>> {
store: Store, const client = await getAdbClient(this.flipperServer.config);
): Promise<Array<BaseDevice>> {
const client = await getAdbClient(store.getState().settingsState);
const androidDevices = await client.listDevices(); const androidDevices = await client.listDevices();
const devices = await Promise.all( const devices = await Promise.all(
androidDevices.map((device) => createDevice(client, device, store)), androidDevices.map((device) => this.createDevice(client, device)),
); );
return devices.filter(Boolean) as any; return devices.filter(Boolean) as any;
} }
function getRunningEmulatorName( async getEmulatorPath(): Promise<string> {
id: string, if (this.emulatorPath) {
): Promise<string | null | undefined> { return this.emulatorPath;
}
// TODO: this doesn't respect the currently configured android_home in settings!
try {
this.emulatorPath = (await promisify(which)('emulator')) as string;
} catch (_e) {
this.emulatorPath = join(
process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || '',
'tools',
'emulator',
);
}
return this.emulatorPath;
}
async getAndroidEmulators(): Promise<string[]> {
const emulatorPath = await this.getEmulatorPath();
return new Promise<string[]>((resolve) => {
child_process.execFile(
emulatorPath as string,
['-list-avds'],
(error: Error | null, data: string | null) => {
if (error != null || data == null) {
console.warn('List AVD failed: ', error);
resolve([]);
return;
}
const devices = data
.split('\n')
.filter(notNull)
.filter((l) => l !== '');
resolve(devices);
},
);
});
}
async getRunningEmulatorName(id: string): Promise<string | null | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const port = id.replace('emulator-', ''); const port = id.replace('emulator-', '');
// The GNU version of netcat doesn't terminate after 1s when // The GNU version of netcat doesn't terminate after 1s when
@@ -137,56 +176,19 @@ function getRunningEmulatorName(
}); });
} }
export default (store: Store, logger: Logger) => { async watchAndroidDevices() {
const watchAndroidDevices = () => { try {
// get emulators const client = await getAdbClient(this.flipperServer.config);
promisify(which)('emulator')
.catch(() =>
join(
process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || '',
'tools',
'emulator',
),
)
.then((emulatorPath) => {
child_process.execFile(
emulatorPath as string,
['-list-avds'],
(error: Error | null, data: string | null) => {
if (error != null || data == null) {
console.warn('List AVD failed: ', error);
return;
}
const payload = data.split('\n').filter(Boolean);
store.dispatch({
type: 'REGISTER_ANDROID_EMULATORS',
payload,
});
},
);
})
.catch((err) => {
console.warn('Failed to query AVDs:', err);
});
return getAdbClient(store.getState().settingsState)
.then((client) => {
client client
.trackDevices() .trackDevices()
.then((tracker) => { .then((tracker) => {
tracker.on('error', (err) => { tracker.on('error', (err) => {
if (err.message === 'Connection closed') { if (err.message === 'Connection closed') {
// adb server has shutdown, remove all android devices this.unregisterDevices(Array.from(this.devices.keys()));
const {connections} = store.getState();
const deviceIDsToRemove: Array<string> = connections.devices
.filter(
(device: BaseDevice) => device instanceof AndroidDevice,
)
.map((device: BaseDevice) => device.serial);
unregisterDevices(deviceIDsToRemove);
console.warn('adb server was shutdown'); console.warn('adb server was shutdown');
setTimeout(watchAndroidDevices, 500); setTimeout(() => {
this.watchAndroidDevices();
}, 500);
} else { } else {
throw err; throw err;
} }
@@ -194,20 +196,20 @@ export default (store: Store, logger: Logger) => {
tracker.on('add', async (device) => { tracker.on('add', async (device) => {
if (device.type !== 'offline') { if (device.type !== 'offline') {
registerDevice(client, device, store); this.registerDevice(client, device);
} }
}); });
tracker.on('change', async (device) => { tracker.on('change', async (device) => {
if (device.type === 'offline') { if (device.type === 'offline') {
unregisterDevices([device.id]); this.unregisterDevices([device.id]);
} else { } else {
registerDevice(client, device, store); this.registerDevice(client, device);
} }
}); });
tracker.on('remove', (device) => { tracker.on('remove', (device) => {
unregisterDevices([device.id]); this.unregisterDevices([device.id]);
}); });
}) })
.catch((err: {code: string}) => { .catch((err: {code: string}) => {
@@ -217,72 +219,31 @@ export default (store: Store, logger: Logger) => {
throw err; throw err;
} }
}); });
}) } catch (e) {
.catch((e) => {
console.warn(`Failed to watch for android devices: ${e.message}`); console.warn(`Failed to watch for android devices: ${e.message}`);
}); }
}; }
async function registerDevice(adbClient: any, deviceData: any, store: Store) { async registerDevice(adbClient: ADBClient, deviceData: any) {
const androidDevice = await createDevice( const androidDevice = await this.createDevice(adbClient, deviceData);
adbClient,
deviceData,
store,
store.getState().application.serverPorts,
);
if (!androidDevice) { if (!androidDevice) {
return; return;
} }
logger.track('usage', 'register-device', {
os: 'Android',
name: androidDevice.title,
serial: androidDevice.serial,
});
// remove offline devices with same serial as the connected. // remove offline devices with same serial as the connected.
const reconnectedDevices = store this.devices.get(androidDevice.serial)?.destroy();
.getState() // register new device
.connections.devices.filter( this.devices.set(androidDevice.serial, androidDevice);
(device: BaseDevice) => this.flipperServer.emit('device-connected', androidDevice);
device.serial === androidDevice.serial && !device.connected.get(),
)
.map((device) => device.serial);
reconnectedDevices.forEach((serial) => {
destroyDevice(store, logger, serial);
});
androidDevice.loadDevicePlugins(
store.getState().plugins.devicePlugins,
store.getState().connections.enabledDevicePlugins,
);
store.dispatch({
type: 'REGISTER_DEVICE',
payload: androidDevice,
});
} }
async function unregisterDevices(deviceIds: Array<string>) { unregisterDevices(serials: Array<string>) {
deviceIds.forEach((id) => serials.forEach((serial) => {
logger.track('usage', 'unregister-device', { const device = this.devices.get(serial);
os: 'Android', if (device?.connected?.get()) {
serial: id, device.disconnect();
}), this.flipperServer.emit('device-disconnected', device);
); }
deviceIds.forEach((id) => {
const device = store
.getState()
.connections.devices.find((device) => device.serial === id);
device?.disconnect();
}); });
} }
}
watchAndroidDevices();
// cleanup method
return () =>
getAdbClient(store.getState().settingsState).then((client) => {
client.kill();
});
};