server_model.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_hbb/consts.dart';
  5. import 'package:flutter_hbb/main.dart';
  6. import 'package:flutter_hbb/mobile/pages/settings_page.dart';
  7. import 'package:flutter_hbb/models/chat_model.dart';
  8. import 'package:flutter_hbb/models/platform_model.dart';
  9. import 'package:get/get.dart';
  10. import 'package:wakelock_plus/wakelock_plus.dart';
  11. import 'package:window_manager/window_manager.dart';
  12. import '../common.dart';
  13. import '../common/formatter/id_formatter.dart';
  14. import '../desktop/pages/server_page.dart' as desktop;
  15. import '../desktop/widgets/tabbar_widget.dart';
  16. import '../mobile/pages/server_page.dart';
  17. import 'model.dart';
  18. const kLoginDialogTag = "LOGIN";
  19. const kUseTemporaryPassword = "use-temporary-password";
  20. const kUsePermanentPassword = "use-permanent-password";
  21. const kUseBothPasswords = "use-both-passwords";
  22. class ServerModel with ChangeNotifier {
  23. bool _isStart = false; // Android MainService status
  24. bool _mediaOk = false;
  25. bool _inputOk = false;
  26. bool _audioOk = false;
  27. bool _fileOk = false;
  28. bool _clipboardOk = false;
  29. bool _showElevation = false;
  30. bool hideCm = false;
  31. int _connectStatus = 0; // Rendezvous Server status
  32. String _verificationMethod = "";
  33. String _temporaryPasswordLength = "";
  34. String _approveMode = "";
  35. int _zeroClientLengthCounter = 0;
  36. late String _emptyIdShow;
  37. late final IDTextEditingController _serverId;
  38. final _serverPasswd =
  39. TextEditingController(text: translate("Generating ..."));
  40. final tabController = DesktopTabController(tabType: DesktopTabType.cm);
  41. final List<Client> _clients = [];
  42. Timer? cmHiddenTimer;
  43. bool get isStart => _isStart;
  44. bool get mediaOk => _mediaOk;
  45. bool get inputOk => _inputOk;
  46. bool get audioOk => _audioOk;
  47. bool get fileOk => _fileOk;
  48. bool get clipboardOk => _clipboardOk;
  49. bool get showElevation => _showElevation;
  50. int get connectStatus => _connectStatus;
  51. String get verificationMethod {
  52. final index = [
  53. kUseTemporaryPassword,
  54. kUsePermanentPassword,
  55. kUseBothPasswords
  56. ].indexOf(_verificationMethod);
  57. if (index < 0) {
  58. return kUseBothPasswords;
  59. }
  60. return _verificationMethod;
  61. }
  62. String get approveMode => _approveMode;
  63. setVerificationMethod(String method) async {
  64. await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
  65. /*
  66. if (method != kUsePermanentPassword) {
  67. await bind.mainSetOption(
  68. key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false));
  69. }
  70. */
  71. }
  72. String get temporaryPasswordLength {
  73. final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength);
  74. if (lengthIndex < 0) {
  75. return "6";
  76. }
  77. return _temporaryPasswordLength;
  78. }
  79. setTemporaryPasswordLength(String length) async {
  80. await bind.mainSetOption(key: "temporary-password-length", value: length);
  81. }
  82. setApproveMode(String mode) async {
  83. await bind.mainSetOption(key: kOptionApproveMode, value: mode);
  84. /*
  85. if (mode != 'password') {
  86. await bind.mainSetOption(
  87. key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false));
  88. }
  89. */
  90. }
  91. TextEditingController get serverId => _serverId;
  92. TextEditingController get serverPasswd => _serverPasswd;
  93. List<Client> get clients => _clients;
  94. final controller = ScrollController();
  95. WeakReference<FFI> parent;
  96. ServerModel(this.parent) {
  97. _emptyIdShow = translate("Generating ...");
  98. _serverId = IDTextEditingController(text: _emptyIdShow);
  99. /*
  100. // initital _hideCm at startup
  101. final verificationMethod =
  102. bind.mainGetOptionSync(key: kOptionVerificationMethod);
  103. final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
  104. _hideCm = option2bool(
  105. 'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
  106. if (!(approveMode == 'password' &&
  107. verificationMethod == kUsePermanentPassword)) {
  108. _hideCm = false;
  109. }
  110. */
  111. timerCallback() async {
  112. final connectionStatus =
  113. jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
  114. final statusNum = connectionStatus['status_num'] as int;
  115. if (statusNum != _connectStatus) {
  116. _connectStatus = statusNum;
  117. notifyListeners();
  118. }
  119. if (desktopType == DesktopType.cm) {
  120. final res = await bind.cmCheckClientsLength(length: _clients.length);
  121. if (res != null) {
  122. debugPrint("clients not match!");
  123. updateClientState(res);
  124. } else {
  125. if (_clients.isEmpty) {
  126. hideCmWindow();
  127. if (_zeroClientLengthCounter++ == 12) {
  128. // 6 second
  129. windowManager.close();
  130. }
  131. } else {
  132. _zeroClientLengthCounter = 0;
  133. if (!hideCm) showCmWindow();
  134. }
  135. }
  136. }
  137. updatePasswordModel();
  138. }
  139. if (!isTest) {
  140. Future.delayed(Duration.zero, () async {
  141. if (await bind.optionSynced()) {
  142. await timerCallback();
  143. }
  144. });
  145. Timer.periodic(Duration(milliseconds: 500), (timer) async {
  146. await timerCallback();
  147. });
  148. }
  149. // Initial keyboard status is off on mobile
  150. if (isMobile) {
  151. bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
  152. }
  153. }
  154. /// 1. check android permission
  155. /// 2. check config
  156. /// audio true by default (if permission on) (false default < Android 10)
  157. /// file true by default (if permission on)
  158. checkAndroidPermission() async {
  159. // audio
  160. if (androidVersion < 30 ||
  161. !await AndroidPermissionManager.check(kRecordAudio)) {
  162. _audioOk = false;
  163. bind.mainSetOption(key: kOptionEnableAudio, value: "N");
  164. } else {
  165. final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
  166. _audioOk = audioOption != 'N';
  167. }
  168. // file
  169. if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
  170. _fileOk = false;
  171. bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
  172. } else {
  173. final fileOption =
  174. await bind.mainGetOption(key: kOptionEnableFileTransfer);
  175. _fileOk = fileOption != 'N';
  176. }
  177. // clipboard
  178. final clipOption = await bind.mainGetOption(key: kOptionEnableClipboard);
  179. _clipboardOk = clipOption != 'N';
  180. notifyListeners();
  181. }
  182. updatePasswordModel() async {
  183. var update = false;
  184. final temporaryPassword = await bind.mainGetTemporaryPassword();
  185. final verificationMethod =
  186. await bind.mainGetOption(key: kOptionVerificationMethod);
  187. final temporaryPasswordLength =
  188. await bind.mainGetOption(key: "temporary-password-length");
  189. final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
  190. /*
  191. var hideCm = option2bool(
  192. 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
  193. if (!(approveMode == 'password' &&
  194. verificationMethod == kUsePermanentPassword)) {
  195. hideCm = false;
  196. }
  197. */
  198. if (_approveMode != approveMode) {
  199. _approveMode = approveMode;
  200. update = true;
  201. }
  202. var stopped = await mainGetBoolOption(kOptionStopService);
  203. final oldPwdText = _serverPasswd.text;
  204. if (stopped ||
  205. verificationMethod == kUsePermanentPassword ||
  206. _approveMode == 'click') {
  207. _serverPasswd.text = '-';
  208. } else {
  209. if (_serverPasswd.text != temporaryPassword &&
  210. temporaryPassword.isNotEmpty) {
  211. _serverPasswd.text = temporaryPassword;
  212. }
  213. }
  214. if (oldPwdText != _serverPasswd.text) {
  215. update = true;
  216. }
  217. if (_verificationMethod != verificationMethod) {
  218. _verificationMethod = verificationMethod;
  219. update = true;
  220. }
  221. if (_temporaryPasswordLength != temporaryPasswordLength) {
  222. if (_temporaryPasswordLength.isNotEmpty) {
  223. bind.mainUpdateTemporaryPassword();
  224. }
  225. _temporaryPasswordLength = temporaryPasswordLength;
  226. update = true;
  227. }
  228. /*
  229. if (_hideCm != hideCm) {
  230. _hideCm = hideCm;
  231. if (desktopType == DesktopType.cm) {
  232. if (hideCm) {
  233. await hideCmWindow();
  234. } else {
  235. await showCmWindow();
  236. }
  237. }
  238. update = true;
  239. }
  240. */
  241. if (update) {
  242. notifyListeners();
  243. }
  244. }
  245. toggleAudio() async {
  246. if (clients.isNotEmpty) {
  247. await showClientsMayNotBeChangedAlert(parent.target);
  248. }
  249. if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
  250. final res = await AndroidPermissionManager.request(kRecordAudio);
  251. if (!res) {
  252. showToast(translate('Failed'));
  253. return;
  254. }
  255. }
  256. _audioOk = !_audioOk;
  257. bind.mainSetOption(
  258. key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
  259. notifyListeners();
  260. }
  261. toggleFile() async {
  262. if (clients.isNotEmpty) {
  263. await showClientsMayNotBeChangedAlert(parent.target);
  264. }
  265. if (!_fileOk &&
  266. !await AndroidPermissionManager.check(kManageExternalStorage)) {
  267. final res =
  268. await AndroidPermissionManager.request(kManageExternalStorage);
  269. if (!res) {
  270. showToast(translate('Failed'));
  271. return;
  272. }
  273. }
  274. _fileOk = !_fileOk;
  275. bind.mainSetOption(
  276. key: kOptionEnableFileTransfer,
  277. value: _fileOk ? defaultOptionYes : 'N');
  278. notifyListeners();
  279. }
  280. toggleClipboard() async {
  281. _clipboardOk = !_clipboardOk;
  282. bind.mainSetOption(
  283. key: kOptionEnableClipboard,
  284. value: _clipboardOk ? defaultOptionYes : 'N');
  285. notifyListeners();
  286. }
  287. toggleInput() async {
  288. if (clients.isNotEmpty) {
  289. await showClientsMayNotBeChangedAlert(parent.target);
  290. }
  291. if (_inputOk) {
  292. parent.target?.invokeMethod("stop_input");
  293. bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
  294. } else {
  295. if (parent.target != null) {
  296. /// the result of toggle-on depends on user actions in the settings page.
  297. /// handle result, see [ServerModel.changeStatue]
  298. showInputWarnAlert(parent.target!);
  299. }
  300. }
  301. }
  302. Future<bool> checkRequestNotificationPermission() async {
  303. debugPrint("androidVersion $androidVersion");
  304. if (androidVersion < 33) {
  305. return true;
  306. }
  307. if (await AndroidPermissionManager.check(kAndroid13Notification)) {
  308. debugPrint("notification permission already granted");
  309. return true;
  310. }
  311. var res = await AndroidPermissionManager.request(kAndroid13Notification);
  312. debugPrint("notification permission request result: $res");
  313. return res;
  314. }
  315. Future<bool> checkFloatingWindowPermission() async {
  316. debugPrint("androidVersion $androidVersion");
  317. if (androidVersion < 23) {
  318. return false;
  319. }
  320. if (await AndroidPermissionManager.check(kSystemAlertWindow)) {
  321. debugPrint("alert window permission already granted");
  322. return true;
  323. }
  324. var res = await AndroidPermissionManager.request(kSystemAlertWindow);
  325. debugPrint("alert window permission request result: $res");
  326. return res;
  327. }
  328. /// Toggle the screen sharing service.
  329. toggleService() async {
  330. if (_isStart) {
  331. final res = await parent.target?.dialogManager
  332. .show<bool>((setState, close, context) {
  333. submit() => close(true);
  334. return CustomAlertDialog(
  335. title: Row(children: [
  336. const Icon(Icons.warning_amber_sharp,
  337. color: Colors.redAccent, size: 28),
  338. const SizedBox(width: 10),
  339. Text(translate("Warning")),
  340. ]),
  341. content: Text(translate("android_stop_service_tip")),
  342. actions: [
  343. TextButton(onPressed: close, child: Text(translate("Cancel"))),
  344. TextButton(onPressed: submit, child: Text(translate("OK"))),
  345. ],
  346. onSubmit: submit,
  347. onCancel: close,
  348. );
  349. });
  350. if (res == true) {
  351. stopService();
  352. }
  353. } else {
  354. await checkRequestNotificationPermission();
  355. if (bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) != 'Y') {
  356. await checkFloatingWindowPermission();
  357. }
  358. if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
  359. await AndroidPermissionManager.request(kManageExternalStorage);
  360. }
  361. final res = await parent.target?.dialogManager
  362. .show<bool>((setState, close, context) {
  363. submit() => close(true);
  364. return CustomAlertDialog(
  365. title: Row(children: [
  366. const Icon(Icons.warning_amber_sharp,
  367. color: Colors.redAccent, size: 28),
  368. const SizedBox(width: 10),
  369. Text(translate("Warning")),
  370. ]),
  371. content: Text(translate("android_service_will_start_tip")),
  372. actions: [
  373. dialogButton("Cancel", onPressed: close, isOutline: true),
  374. dialogButton("OK", onPressed: submit),
  375. ],
  376. onSubmit: submit,
  377. onCancel: close,
  378. );
  379. });
  380. if (res == true) {
  381. startService();
  382. }
  383. }
  384. }
  385. /// Start the screen sharing service.
  386. Future<void> startService() async {
  387. _isStart = true;
  388. notifyListeners();
  389. parent.target?.ffiModel.updateEventListener(parent.target!.sessionId, "");
  390. await parent.target?.invokeMethod("init_service");
  391. // ugly is here, because for desktop, below is useless
  392. await bind.mainStartService();
  393. updateClientState();
  394. if (isAndroid) {
  395. androidUpdatekeepScreenOn();
  396. }
  397. }
  398. /// Stop the screen sharing service.
  399. Future<void> stopService() async {
  400. _isStart = false;
  401. closeAll();
  402. await parent.target?.invokeMethod("stop_service");
  403. await bind.mainStopService();
  404. notifyListeners();
  405. if (!isLinux) {
  406. // current linux is not supported
  407. WakelockPlus.disable();
  408. }
  409. }
  410. Future<bool> setPermanentPassword(String newPW) async {
  411. await bind.mainSetPermanentPassword(password: newPW);
  412. await Future.delayed(Duration(milliseconds: 500));
  413. final pw = await bind.mainGetPermanentPassword();
  414. if (newPW == pw) {
  415. return true;
  416. } else {
  417. return false;
  418. }
  419. }
  420. fetchID() async {
  421. final id = await bind.mainGetMyId();
  422. if (id != _serverId.id) {
  423. _serverId.id = id;
  424. notifyListeners();
  425. }
  426. }
  427. changeStatue(String name, bool value) {
  428. debugPrint("changeStatue value $value");
  429. switch (name) {
  430. case "media":
  431. _mediaOk = value;
  432. if (value && !_isStart) {
  433. startService();
  434. }
  435. break;
  436. case "input":
  437. if (_inputOk != value) {
  438. bind.mainSetOption(
  439. key: kOptionEnableKeyboard,
  440. value: value ? defaultOptionYes : 'N');
  441. }
  442. _inputOk = value;
  443. break;
  444. default:
  445. return;
  446. }
  447. notifyListeners();
  448. }
  449. // force
  450. updateClientState([String? json]) async {
  451. if (isTest) return;
  452. var res = await bind.cmGetClientsState();
  453. List<dynamic> clientsJson;
  454. try {
  455. clientsJson = jsonDecode(res);
  456. } catch (e) {
  457. debugPrint("Failed to decode clientsJson: '$res', error $e");
  458. return;
  459. }
  460. final oldClientLenght = _clients.length;
  461. _clients.clear();
  462. tabController.state.value.tabs.clear();
  463. for (var clientJson in clientsJson) {
  464. try {
  465. final client = Client.fromJson(clientJson);
  466. _clients.add(client);
  467. _addTab(client);
  468. } catch (e) {
  469. debugPrint("Failed to decode clientJson '$clientJson', error $e");
  470. }
  471. }
  472. if (desktopType == DesktopType.cm) {
  473. if (_clients.isEmpty) {
  474. hideCmWindow();
  475. } else if (!hideCm) {
  476. showCmWindow();
  477. }
  478. }
  479. if (_clients.length != oldClientLenght) {
  480. notifyListeners();
  481. if (isAndroid) androidUpdatekeepScreenOn();
  482. }
  483. }
  484. void addConnection(Map<String, dynamic> evt) {
  485. try {
  486. final client = Client.fromJson(jsonDecode(evt["client"]));
  487. if (client.authorized) {
  488. parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id));
  489. final index = _clients.indexWhere((c) => c.id == client.id);
  490. if (index < 0) {
  491. _clients.add(client);
  492. } else {
  493. _clients[index].authorized = true;
  494. }
  495. } else {
  496. if (_clients.any((c) => c.id == client.id)) {
  497. return;
  498. }
  499. _clients.add(client);
  500. }
  501. _addTab(client);
  502. // remove disconnected
  503. final index_disconnected = _clients
  504. .indexWhere((c) => c.disconnected && c.peerId == client.peerId);
  505. if (index_disconnected >= 0) {
  506. _clients.removeAt(index_disconnected);
  507. tabController.remove(index_disconnected);
  508. }
  509. if (desktopType == DesktopType.cm && !hideCm) {
  510. showCmWindow();
  511. }
  512. scrollToBottom();
  513. notifyListeners();
  514. if (isAndroid && !client.authorized) showLoginDialog(client);
  515. if (isAndroid) androidUpdatekeepScreenOn();
  516. } catch (e) {
  517. debugPrint("Failed to call loginRequest,error:$e");
  518. }
  519. }
  520. void _addTab(Client client) {
  521. tabController.add(TabInfo(
  522. key: client.id.toString(),
  523. label: client.name,
  524. closable: false,
  525. onTap: () {},
  526. page: desktop.buildConnectionCard(client)));
  527. Future.delayed(Duration.zero, () async {
  528. if (!hideCm) windowOnTop(null);
  529. });
  530. // Only do the hidden task when on Desktop.
  531. if (client.authorized && isDesktop) {
  532. cmHiddenTimer = Timer(const Duration(seconds: 3), () {
  533. if (!hideCm) windowManager.minimize();
  534. cmHiddenTimer = null;
  535. });
  536. }
  537. parent.target?.chatModel
  538. .updateConnIdOfKey(MessageKey(client.peerId, client.id));
  539. }
  540. void showLoginDialog(Client client) {
  541. showClientDialog(
  542. client,
  543. client.isFileTransfer ? "File Connection" : "Screen Connection",
  544. 'Do you accept?',
  545. 'android_new_connection_tip',
  546. () => sendLoginResponse(client, false),
  547. () => sendLoginResponse(client, true),
  548. );
  549. }
  550. handleVoiceCall(Client client, bool accept) {
  551. parent.target?.invokeMethod("cancel_notification", client.id);
  552. bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
  553. }
  554. showVoiceCallDialog(Client client) {
  555. showClientDialog(
  556. client,
  557. 'Voice call',
  558. 'Do you accept?',
  559. 'android_new_voice_call_tip',
  560. () => handleVoiceCall(client, false),
  561. () => handleVoiceCall(client, true),
  562. );
  563. }
  564. showClientDialog(Client client, String title, String contentTitle,
  565. String content, VoidCallback onCancel, VoidCallback onSubmit) {
  566. parent.target?.dialogManager.show((setState, close, context) {
  567. cancel() {
  568. onCancel();
  569. close();
  570. }
  571. submit() {
  572. onSubmit();
  573. close();
  574. }
  575. return CustomAlertDialog(
  576. title:
  577. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  578. Text(translate(title)),
  579. IconButton(onPressed: close, icon: const Icon(Icons.close))
  580. ]),
  581. content: Column(
  582. mainAxisSize: MainAxisSize.min,
  583. mainAxisAlignment: MainAxisAlignment.center,
  584. crossAxisAlignment: CrossAxisAlignment.start,
  585. children: [
  586. Text(translate(contentTitle)),
  587. ClientInfo(client),
  588. Text(
  589. translate(content),
  590. style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
  591. ),
  592. ],
  593. ),
  594. actions: [
  595. dialogButton("Dismiss", onPressed: cancel, isOutline: true),
  596. if (approveMode != 'password')
  597. dialogButton("Accept", onPressed: submit),
  598. ],
  599. onSubmit: submit,
  600. onCancel: cancel,
  601. );
  602. }, tag: getLoginDialogTag(client.id));
  603. }
  604. scrollToBottom() {
  605. if (isDesktop) return;
  606. Future.delayed(Duration(milliseconds: 200), () {
  607. controller.animateTo(controller.position.maxScrollExtent,
  608. duration: Duration(milliseconds: 200),
  609. curve: Curves.fastLinearToSlowEaseIn);
  610. });
  611. }
  612. void sendLoginResponse(Client client, bool res) async {
  613. if (res) {
  614. bind.cmLoginRes(connId: client.id, res: res);
  615. if (!client.isFileTransfer) {
  616. parent.target?.invokeMethod("start_capture");
  617. }
  618. parent.target?.invokeMethod("cancel_notification", client.id);
  619. client.authorized = true;
  620. notifyListeners();
  621. } else {
  622. bind.cmLoginRes(connId: client.id, res: res);
  623. parent.target?.invokeMethod("cancel_notification", client.id);
  624. final index = _clients.indexOf(client);
  625. tabController.remove(index);
  626. _clients.remove(client);
  627. if (isAndroid) androidUpdatekeepScreenOn();
  628. }
  629. }
  630. void onClientRemove(Map<String, dynamic> evt) {
  631. try {
  632. final id = int.parse(evt['id'] as String);
  633. final close = (evt['close'] as String) == 'true';
  634. if (_clients.any((c) => c.id == id)) {
  635. final index = _clients.indexWhere((client) => client.id == id);
  636. if (index >= 0) {
  637. if (close) {
  638. _clients.removeAt(index);
  639. tabController.remove(index);
  640. } else {
  641. _clients[index].disconnected = true;
  642. }
  643. }
  644. parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
  645. parent.target?.invokeMethod("cancel_notification", id);
  646. }
  647. if (desktopType == DesktopType.cm && _clients.isEmpty) {
  648. hideCmWindow();
  649. }
  650. if (isAndroid) androidUpdatekeepScreenOn();
  651. notifyListeners();
  652. } catch (e) {
  653. debugPrint("onClientRemove failed,error:$e");
  654. }
  655. }
  656. Future<void> closeAll() async {
  657. await Future.wait(
  658. _clients.map((client) => bind.cmCloseConnection(connId: client.id)));
  659. _clients.clear();
  660. tabController.state.value.tabs.clear();
  661. if (isAndroid) androidUpdatekeepScreenOn();
  662. }
  663. void jumpTo(int id) {
  664. final index = _clients.indexWhere((client) => client.id == id);
  665. tabController.jumpTo(index);
  666. }
  667. void setShowElevation(bool show) {
  668. if (_showElevation != show) {
  669. _showElevation = show;
  670. notifyListeners();
  671. }
  672. }
  673. void updateVoiceCallState(Map<String, dynamic> evt) {
  674. try {
  675. final client = Client.fromJson(jsonDecode(evt["client"]));
  676. final index = _clients.indexWhere((element) => element.id == client.id);
  677. if (index != -1) {
  678. _clients[index].inVoiceCall = client.inVoiceCall;
  679. _clients[index].incomingVoiceCall = client.incomingVoiceCall;
  680. if (client.incomingVoiceCall) {
  681. if (isAndroid) {
  682. showVoiceCallDialog(client);
  683. } else {
  684. // Has incoming phone call, let's set the window on top.
  685. Future.delayed(Duration.zero, () {
  686. windowOnTop(null);
  687. });
  688. }
  689. }
  690. notifyListeners();
  691. }
  692. } catch (e) {
  693. debugPrint("updateVoiceCallState failed: $e");
  694. }
  695. }
  696. void androidUpdatekeepScreenOn() async {
  697. if (!isAndroid) return;
  698. var floatingWindowDisabled =
  699. bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
  700. !await AndroidPermissionManager.check(kSystemAlertWindow);
  701. final keepScreenOn = floatingWindowDisabled
  702. ? KeepScreenOn.never
  703. : optionToKeepScreenOn(
  704. bind.mainGetLocalOption(key: kOptionKeepScreenOn));
  705. final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) ||
  706. (keepScreenOn == KeepScreenOn.duringControlled &&
  707. _clients.map((e) => !e.disconnected).isNotEmpty);
  708. if (on != await WakelockPlus.enabled) {
  709. if (on) {
  710. WakelockPlus.enable();
  711. } else {
  712. WakelockPlus.disable();
  713. }
  714. }
  715. }
  716. }
  717. enum ClientType {
  718. remote,
  719. file,
  720. portForward,
  721. }
  722. class Client {
  723. int id = 0; // client connections inner count id
  724. bool authorized = false;
  725. bool isFileTransfer = false;
  726. String portForward = "";
  727. String name = "";
  728. String peerId = ""; // peer user's id,show at app
  729. bool keyboard = false;
  730. bool clipboard = false;
  731. bool audio = false;
  732. bool file = false;
  733. bool restart = false;
  734. bool recording = false;
  735. bool blockInput = false;
  736. bool disconnected = false;
  737. bool fromSwitch = false;
  738. bool inVoiceCall = false;
  739. bool incomingVoiceCall = false;
  740. RxInt unreadChatMessageCount = 0.obs;
  741. Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId,
  742. this.keyboard, this.clipboard, this.audio);
  743. Client.fromJson(Map<String, dynamic> json) {
  744. id = json['id'];
  745. authorized = json['authorized'];
  746. isFileTransfer = json['is_file_transfer'];
  747. portForward = json['port_forward'];
  748. name = json['name'];
  749. peerId = json['peer_id'];
  750. keyboard = json['keyboard'];
  751. clipboard = json['clipboard'];
  752. audio = json['audio'];
  753. file = json['file'];
  754. restart = json['restart'];
  755. recording = json['recording'];
  756. blockInput = json['block_input'];
  757. disconnected = json['disconnected'];
  758. fromSwitch = json['from_switch'];
  759. inVoiceCall = json['in_voice_call'];
  760. incomingVoiceCall = json['incoming_voice_call'];
  761. }
  762. Map<String, dynamic> toJson() {
  763. final Map<String, dynamic> data = <String, dynamic>{};
  764. data['id'] = id;
  765. data['authorized'] = authorized;
  766. data['is_file_transfer'] = isFileTransfer;
  767. data['port_forward'] = portForward;
  768. data['name'] = name;
  769. data['peer_id'] = peerId;
  770. data['keyboard'] = keyboard;
  771. data['clipboard'] = clipboard;
  772. data['audio'] = audio;
  773. data['file'] = file;
  774. data['restart'] = restart;
  775. data['recording'] = recording;
  776. data['block_input'] = blockInput;
  777. data['disconnected'] = disconnected;
  778. data['from_switch'] = fromSwitch;
  779. data['in_voice_call'] = inVoiceCall;
  780. data['incoming_voice_call'] = incomingVoiceCall;
  781. return data;
  782. }
  783. ClientType type_() {
  784. if (isFileTransfer) {
  785. return ClientType.file;
  786. } else if (portForward.isNotEmpty) {
  787. return ClientType.portForward;
  788. } else {
  789. return ClientType.remote;
  790. }
  791. }
  792. }
  793. String getLoginDialogTag(int id) {
  794. return kLoginDialogTag + id.toString();
  795. }
  796. showInputWarnAlert(FFI ffi) {
  797. ffi.dialogManager.show((setState, close, context) {
  798. submit() {
  799. AndroidPermissionManager.startAction(kActionAccessibilitySettings);
  800. close();
  801. }
  802. return CustomAlertDialog(
  803. title: Text(translate("How to get Android input permission?")),
  804. content: Column(
  805. mainAxisSize: MainAxisSize.min,
  806. children: [
  807. Text(translate("android_input_permission_tip1")),
  808. const SizedBox(height: 10),
  809. Text(translate("android_input_permission_tip2")),
  810. ],
  811. ),
  812. actions: [
  813. dialogButton("Cancel", onPressed: close, isOutline: true),
  814. dialogButton("Open System Setting", onPressed: submit),
  815. ],
  816. onSubmit: submit,
  817. onCancel: close,
  818. );
  819. });
  820. }
  821. Future<void> showClientsMayNotBeChangedAlert(FFI? ffi) async {
  822. await ffi?.dialogManager.show((setState, close, context) {
  823. return CustomAlertDialog(
  824. title: Text(translate("Permissions")),
  825. content: Column(
  826. mainAxisSize: MainAxisSize.min,
  827. children: [
  828. Text(translate("android_permission_may_not_change_tip")),
  829. ],
  830. ),
  831. actions: [
  832. dialogButton("OK", onPressed: close),
  833. ],
  834. onSubmit: close,
  835. onCancel: close,
  836. );
  837. });
  838. }