format import AndroidDevice from './AndroidDevice' ; import KaiOSDevice from './KaiOSDevice' ; import child_process from 'child_process' ; import { getAdbClient } from './adbClient' ; import which from 'which' ; import { promisify } from 'util' ; import { Client as ADBClient , Device } from 'adbkit' ; import { join } from 'path' ; import { FlipperServerImpl } from '../../FlipperServerImpl' ; import { notNull } from '../../utils/typeUtils' ; import { getServerPortsConfig , getFlipperServerConfig , } from '../../FlipperServerConfig' ; export class AndroidDeviceManager { constructor ( public flipperServer ) { } private createDevice ( adbClient , device ) { return new Promise ( async ( resolve , reject ) => { const type = device . type !== 'device' || device . id . startsWith ( 'emulator' ) ? 'emulator' : 'physical' ; try { const props = await adbClient . getProperties ( device . id ) ; try { let name = props [ 'ro.product.model' ] ; const abiString = props [ 'ro.product.cpu.abilist' ] || '' ; const sdkVersion = props [ 'ro.build.version.sdk' ] || '' ; const abiList = abiString . length > 0 ? abiString . split ( ',' ) : [ ] ; if ( type === 'emulator' ) { name = ( await this . getRunningEmulatorName ( device . id ) ) || name ; } const isKaiOSDevice = Object . keys ( props ) . some ( ( name ) => name . startsWith ( 'kaios' ) || name . startsWith ( 'ro.kaios' ) ) ; const androidLikeDevice = new ( isKaiOSDevice ? KaiOSDevice : AndroidDevice ) ( this . flipperServer , device . id , type , name , adbClient , abiList , sdkVersion ) ; const ports = getServerPortsConfig ( ) ; if ( ports . serverPorts ) { await androidLikeDevice . reverse ( [ ports . serverPorts . secure , ports . serverPorts . insecure , ports . altServerPorts . secure , ports . altServerPorts . insecure , ] ) . catch ( ( e ) => { console . warn ( `Failed to reverse-proxy ports on device ${ androidLikeDevice . serial }: ${ e }` ) ; } ) ; } if ( type === 'physical' ) { await androidLikeDevice . reverse ( [ 8097 ] ) . catch ( ( e ) => { console . warn ( `Failed to reverse-proxy React DevTools port 8097 on ${ androidLikeDevice . serial }` , e ) ; } ) ; } resolve ( androidLikeDevice ) ; } catch ( e ) { reject ( e ) ; } } catch ( e ) { if ( e && e . message && e . message === `Failure: 'device still connecting'` ) { console . debug ( 'Device still connecting: ' + device . id ) ; } else { const isAuthorizationError = ( e ?. message as string ) ?. includes ( 'device unauthorized' ) ; if ( ! isAuthorizationError ) { console . error ( 'Failed to connect to android device' , e ) ; } this . flipperServer . emit ( 'notification' , { type : 'error' , title : 'Could not connect to ' + device . id , description : isAuthorizationError ? 'Make sure to authorize debugging on the phone' : 'Failed to setup connection: ' + e , } ) ; } resolve ( undefined ) ; } } ) ; } getEmulatorPath ( ) { return join ( this . flipperServer . config . settings . androidHome , 'emulator' , 'emulator' ) ; } async getAndroidEmulators ( ) { const emulatorPath = this . getEmulatorPath ( ) ; return new Promise < string [ ] > ( ( resolve ) => { child_process . execFile ( emulatorPath as string , [ '-list-avds' ] , ( error , data ) => { if ( error != null || data == null ) { console . warn ( 'List AVD failed: ' , error ) ; resolve ( [ ] ) ; return ; } const devices = data . split ( /\r?\n/ ) . filter ( notNull ) . filter ( ( l ) => l !== '' ) ; resolve ( devices ) ; } ) ; } ) ; } private async getRunningEmulatorName ( id ) { return new Promise ( ( resolve , reject ) => { const port = id . replace ( 'emulator-' , '' ) ; child_process . exec ( `echo "avd name" | nc -w 1 localhost ${ port }` , { timeout : 1000 , encoding : 'utf-8' } , ( error , data ) => { if ( data != null && typeof data === 'string' ) { const match = data . trim ( ) . match ( /(.*)\r\nOK$/ ) ; resolve ( match != null && match . length > 0 ? match [ 1 ] : null ) ; } else { reject ( error ) ; } } ) ; } ) ; } async watchAndroidDevices ( ) { try { const client = await getAdbClient ( getFlipperServerConfig ( ) . settings ) ; client . trackDevices ( ) . then ( ( tracker ) => { tracker . on ( 'error' , ( err ) => { if ( err . message === 'Connection closed' ) { console . warn ( 'adb server was shutdown' ) ; this . flipperServer . getDevices ( ) . filter ( ( d ) => d instanceof AndroidDevice ) . forEach ( ( d ) => { this . flipperServer . unregisterDevice ( d . serial ) ; } ) ; setTimeout ( ( ) => { this . watchAndroidDevices ( ) ; } , 500 ) ; } else { throw err ; } } ) ; tracker . on ( 'add' , async ( device ) => { if ( device . type !== 'offline' ) { this . registerDevice ( client , device ) ; } else { console . warn ( `[conn] Found device ${ device . id }, but it has status offline. If this concerns an emulator and the problem persists, try these solutins: https://stackoverflow.com/a/21330228/1983583, https://stackoverflow.com/a/56053223/1983583` ) ; } } ) ; tracker . on ( 'change' , async ( device ) => { if ( device . type === 'offline' ) { this . flipperServer . unregisterDevice ( device . id ) ; } else { this . registerDevice ( client , device ) ; } } ) ; tracker . on ( 'remove' , ( device ) => { this . flipperServer . unregisterDevice ( device . id ) ; } ) ; } ) . catch ( ( err ) => { if ( err . code === 'ECONNREFUSED' ) { console . warn ( 'adb server not running' ) ; } else { throw err ; } } ) ; } catch ( e ) { console . warn ( `Failed to watch for android devices: ${ e . message }` ) ; } } private async registerDevice ( adbClient , deviceData ) { const androidDevice = await this . createDevice ( adbClient , deviceData ) ; if ( ! androidDevice ) { return ; } this . flipperServer . registerDevice ( androidDevice ) ; } }