server_model.dart 28 KB

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