import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; import '../common.dart'; import '../generated_bridge.dart'; class RgbaFrame extends Struct { @Uint32() external int len; external Pointer data; } typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. /// Hides the platform differences. class PlatformFFI { String _dir = ''; // _homeDir is only needed for Android and IOS. String _homeDir = ''; F2? _translate; final _eventHandlers = >{}; late RustdeskImpl _ffiBind; late String _appType; StreamEventHandler? _eventCallback; PlatformFFI._(); static final PlatformFFI instance = PlatformFFI._(); final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; static get localeName => Platform.localeName; static get isMain => instance._appType == kAppTypeMain; static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; } bool registerEventHandler( String eventName, String handlerName, HandleEvent handler) { debugPrint('registerEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers == null) { _eventHandlers[eventName] = {handlerName: handler}; return true; } else { if (handlers.containsKey(handlerName)) { return false; } else { handlers[handlerName] = handler; return true; } } } void unregisterEventHandler(String eventName, String handlerName) { debugPrint('unregisterEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers != null) { handlers.remove(handlerName); } } String translate(String name, String locale) { if (_translate == null) return name; var a = name.toNativeUtf8(); var b = locale.toNativeUtf8(); var p = _translate!(a, b); assert(p != nullptr); final res = p.toDartString(); calloc.free(p); calloc.free(a); calloc.free(b); return res; } /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') : Platform.isLinux ? DynamicLibrary.open('librustdesk.so') : Platform.isWindows ? DynamicLibrary.open('librustdesk.dll') : Platform.isMacOS ? DynamicLibrary.open("liblibrustdesk.dylib") : DynamicLibrary.process(); debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; } catch (e) { debugPrint('Failed to get documents directory: $e'); } _ffiBind = RustdeskImpl(dylib); if (Platform.isLinux) { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); } else if (Platform.isMacOS && isMain) { Future.wait([ // Start dbus service. _ffiBind.mainStartDbusServer(), // Start local audio pulseaudio server. _ffiBind.mainStartPa() ]); } _startListenEvent(_ffiBind); // global event try { if (isAndroid) { // only support for android _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; } else if (isIOS) { _homeDir = _ffiBind.mainGetDataDirIos(); } else { // no need to set home dir } } catch (e) { debugPrintStack(label: 'initialize failed: $e'); } String id = 'NA'; String name = 'Flutter'; DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); androidVersion = androidInfo.version.sdkInt ?? 0; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; name = iosInfo.utsname.machine ?? ''; id = iosInfo.identifierForVendor.hashCode.toString(); } else if (Platform.isLinux) { LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; name = linuxInfo.name; id = linuxInfo.machineId ?? linuxInfo.id; } else if (Platform.isWindows) { try { // request windows build number to fix overflow on win7 windowsBuildNumber = getWindowsTargetBuildNumber(); WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; name = winInfo.computerName; id = winInfo.computerName; } catch (e) { debugPrintStack(label: "get windows device info failed: $e"); name = "unknown"; id = "unknown"; } } else if (Platform.isMacOS) { MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo; name = macOsInfo.computerName; id = macOsInfo.systemGUID ?? ''; } if (isAndroid || isIOS) { debugPrint( '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); } else { debugPrint( '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir'); } await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { debugPrintStack(label: 'initialize failed: $e'); } version = await getVersion(); } Future _tryHandle(Map evt) async { final name = evt['name']; if (name != null) { final handlers = _eventHandlers[name]; if (handlers != null) { if (handlers.isNotEmpty) { for (var handler in handlers.values) { await handler(evt); } return true; } } } return false; } /// Start listening to the Rust core's events and frames. void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { await for (final message in rustdeskImpl.startGlobalEventStream(appType: _appType)) { try { Map event = json.decode(message); // _tryHandle here may be more flexible than _eventCallback if (!await _tryHandle(event)) { if (_eventCallback != null) { await _eventCallback!(event); } } } catch (e) { debugPrint('json.decode fail(): $e'); } } }(); } void setEventCallback(StreamEventHandler fun) async { _eventCallback = fun; } void setRgbaCallback(void Function(Uint8List) fun) async {} void startDesktopWebListener() {} void stopDesktopWebListener() {} void setMethodCallHandler(FMethod callback) { _toAndroidChannel.setMethodCallHandler((call) async { callback(call.method, call.arguments); return null; }); } invokeMethod(String method, [dynamic arguments]) async { if (!isAndroid) return Future(() => false); return await _toAndroidChannel.invokeMethod(method, arguments); } }