webViewRunner.dart 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter_inappwebview/flutter_inappwebview.dart';
  4. import 'package:polkawallet_sdk/api/types/networkParams.dart';
  5. import 'package:polkawallet_sdk/service/localServer.dart';
  6. class WebViewRunner {
  7. HeadlessInAppWebView? _web;
  8. Function? _onLaunched;
  9. String? _jsCode;
  10. Map<String, Function> _msgHandlers = {};
  11. Map<String, Completer> _msgCompleters = {};
  12. Map<String, Function> _reloadHandlers = {};
  13. Map<String, String> _msgJavascript = {};
  14. int _evalJavascriptUID = 0;
  15. bool webViewLoaded = false;
  16. int jsCodeStarted = -1;
  17. bool _webViewOOMReload = false;
  18. Future<void> launch(
  19. Function? onLaunched, {
  20. String? jsCode,
  21. Function? socketDisconnectedAction,
  22. }) async {
  23. /// reset state before webView launch or reload
  24. _msgHandlers = {};
  25. _msgCompleters = {};
  26. _msgJavascript = {};
  27. _reloadHandlers = {};
  28. _evalJavascriptUID = 0;
  29. if (onLaunched != null) {
  30. _onLaunched = onLaunched;
  31. }
  32. webViewLoaded = false;
  33. _webViewOOMReload = false;
  34. jsCodeStarted = -1;
  35. _jsCode = jsCode;
  36. if (_web == null) {
  37. await LocalServer.getInstance().startLocalServer();
  38. _web = new HeadlessInAppWebView(
  39. initialOptions: InAppWebViewGroupOptions(
  40. crossPlatform: InAppWebViewOptions(clearCache: true),
  41. android: AndroidInAppWebViewOptions(useOnRenderProcessGone: true),
  42. ),
  43. androidOnRenderProcessGone: (webView, detail) async {
  44. if (_web?.webViewController == webView) {
  45. webViewLoaded = false;
  46. _webViewOOMReload = true;
  47. await _web?.webViewController.clearCache();
  48. await _web?.webViewController.reload();
  49. }
  50. },
  51. initialUrlRequest: URLRequest(
  52. url: Uri.parse(
  53. "http://localhost:8080/packages/polkawallet_sdk/assets/index.html")),
  54. onWebViewCreated: (controller) {
  55. print('HeadlessInAppWebView created!');
  56. },
  57. onConsoleMessage: (controller, message) {
  58. print("CONSOLE MESSAGE: " + message.message);
  59. if (jsCodeStarted < 0) {
  60. try {
  61. final msg = jsonDecode(message.message);
  62. if (msg['path'] == 'log') {
  63. if (message.message.contains('js loaded')) {
  64. jsCodeStarted = 1;
  65. } else {
  66. jsCodeStarted = 0;
  67. }
  68. }
  69. } catch (err) {
  70. // ignore
  71. }
  72. }
  73. if (message.message.contains("WebSocket is not connected") &&
  74. socketDisconnectedAction != null) {
  75. socketDisconnectedAction();
  76. }
  77. if (message.messageLevel != ConsoleMessageLevel.LOG) return;
  78. try {
  79. var msg = jsonDecode(message.message);
  80. final String path = msg['path']!;
  81. final error = msg['error'];
  82. if (error != null) {
  83. if (_msgCompleters[path] != null) {
  84. Completer handler = _msgCompleters[path]!;
  85. handler.completeError(error);
  86. if (path.contains('uid=')) {
  87. _msgCompleters.remove(path);
  88. }
  89. }
  90. }
  91. if (_msgCompleters[path] != null) {
  92. Completer handler = _msgCompleters[path]!;
  93. handler.complete(msg['data']);
  94. if (path.contains('uid=')) {
  95. _msgCompleters.remove(path);
  96. }
  97. }
  98. if (_msgHandlers[path] != null) {
  99. Function handler = _msgHandlers[path]!;
  100. handler(msg['data']);
  101. }
  102. if (_msgJavascript[path] != null) {
  103. _msgJavascript.remove(path);
  104. }
  105. } catch (err) {
  106. // ignore
  107. print('msg parsing error $err');
  108. }
  109. },
  110. onLoadStop: (controller, url) async {
  111. print('webview loaded $url');
  112. final jsLoaded = await _web!.webViewController
  113. .evaluateJavascript(source: '!!account;');
  114. if (webViewLoaded) return;
  115. if (jsLoaded == true) {
  116. webViewLoaded = true;
  117. await _startJSCode();
  118. }
  119. },
  120. onLoadError: (controller, url, code, message) {
  121. print("webview restart");
  122. _web = null;
  123. launch(null,
  124. jsCode: jsCode,
  125. socketDisconnectedAction: socketDisconnectedAction);
  126. },
  127. );
  128. await _web?.dispose();
  129. await _web?.run();
  130. } else {
  131. _tryReload();
  132. }
  133. }
  134. void _tryReload() {
  135. if (!webViewLoaded) {
  136. _web?.webViewController.reload();
  137. }
  138. }
  139. Future<void> _startJSCode() async {
  140. // inject js file to webView
  141. if (_jsCode != null) {
  142. await _web!.webViewController.evaluateJavascript(source: _jsCode!);
  143. }
  144. _onLaunched!();
  145. _reloadHandlers.forEach((_, value) {
  146. value();
  147. });
  148. }
  149. int getEvalJavascriptUID() {
  150. return _evalJavascriptUID++;
  151. }
  152. Future<dynamic> evalJavascript(
  153. String code, {
  154. bool wrapPromise = true,
  155. bool allowRepeat = true,
  156. }) async {
  157. // check if there's a same request loading
  158. if (!allowRepeat) {
  159. for (String i in _msgCompleters.keys) {
  160. String call = code.split('(')[0];
  161. if (i.contains(call)) {
  162. print('request $call loading');
  163. return _msgCompleters[i]!.future;
  164. }
  165. }
  166. }
  167. if (!wrapPromise) {
  168. final res =
  169. await _web!.webViewController.evaluateJavascript(source: code);
  170. return res;
  171. }
  172. final c = new Completer();
  173. final uid = getEvalJavascriptUID();
  174. final jsCall = code.split('(');
  175. final method = 'uid=$uid;${jsCall[0]}';
  176. _msgCompleters[method] = c;
  177. final script = '$code.then(function(res) {'
  178. ' console.log(JSON.stringify({ path: "$method", data: res }));'
  179. '}).catch(function(err) {'
  180. ' console.log(JSON.stringify({ path: "$method", error: err.message }));'
  181. '});';
  182. _web!.webViewController.evaluateJavascript(source: script);
  183. _msgJavascript[jsCall[0]] = script;
  184. return c.future;
  185. }
  186. Future<NetworkParams?> connectNode(List<NetworkParams> nodes) async {
  187. final isAvatarSupport = (await evalJavascript(
  188. 'settings.connectAll ? {}:null',
  189. wrapPromise: false)) !=
  190. null;
  191. final dynamic res = await (isAvatarSupport
  192. ? evalJavascript(
  193. 'settings.connectAll(${jsonEncode(nodes.map((e) => e.endpoint).toList())})')
  194. : evalJavascript(
  195. 'settings.connect(${jsonEncode(nodes.map((e) => e.endpoint).toList())})'));
  196. if (res != null) {
  197. final index = nodes.indexWhere((e) => e.endpoint!.trim() == res.trim());
  198. if (_webViewOOMReload) {
  199. print(
  200. "webView OOM Reload evaluateJavascript====\n${_msgJavascript.keys.toString()}");
  201. _msgJavascript.forEach((key, value) {
  202. _web!.webViewController.evaluateJavascript(source: value);
  203. });
  204. _msgJavascript = {};
  205. _webViewOOMReload = false;
  206. }
  207. return nodes[index > -1 ? index : 0];
  208. }
  209. return null;
  210. }
  211. Future<NetworkParams?> connectEVM(NetworkParams node) async {
  212. final Map? res =
  213. await (evalJavascript('eth.settings.connect("${node.endpoint}")'));
  214. if (res != null) {
  215. if (_webViewOOMReload) {
  216. print(
  217. "webView OOM Reload evaluateJavascript====\n${_msgJavascript.keys.toString()}");
  218. _msgJavascript.forEach((key, value) {
  219. _web!.webViewController.evaluateJavascript(source: value);
  220. });
  221. _msgJavascript = {};
  222. _webViewOOMReload = false;
  223. }
  224. node.chainId = res['chainId'].toString();
  225. return node;
  226. }
  227. return null;
  228. }
  229. Future<void> subscribeMessage(
  230. String code,
  231. String channel,
  232. Function callback,
  233. ) async {
  234. addMsgHandler(channel, callback);
  235. evalJavascript(code);
  236. }
  237. void unsubscribeMessage(String channel) {
  238. print('unsubscribe $channel');
  239. final unsubCall = 'unsub$channel';
  240. _web!.webViewController
  241. .evaluateJavascript(source: 'window.$unsubCall && window.$unsubCall()');
  242. }
  243. void addMsgHandler(String channel, Function onMessage) {
  244. _msgHandlers[channel] = onMessage;
  245. }
  246. void removeMsgHandler(String channel) {
  247. _msgHandlers.remove(channel);
  248. }
  249. void subscribeReloadAction(String reloadKey, Function reloadAction) {
  250. _reloadHandlers[reloadKey] = reloadAction;
  251. }
  252. void unsubscribeReloadAction(String reloadKey) {
  253. _reloadHandlers.remove(reloadKey);
  254. }
  255. }