import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:together_mobile/database/hive_database.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; import 'package:together_mobile/models/route_state_model.dart'; import 'package:together_mobile/database/box_type.dart'; import 'package:together_mobile/models/apply_list_model.dart'; import 'package:together_mobile/models/contact_model.dart'; import 'package:together_mobile/models/init_get_it.dart'; import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/notification_api.dart'; enum SocketStatus { connected, reconnecting, error, closed, } class WebSocketManager extends ChangeNotifier { late Uri wsUrl; late WebSocketChannel channel; String id = ''; SocketStatus socketStatus = SocketStatus.closed; Timer? heartBeatTimer; Timer? serverTimer; Timer? reconnectTimer; int reconnectCount = 5; int reconnectTimes = 0; Duration heartBeatTimeout = const Duration(seconds: 4); Duration reconnectTimeout = const Duration(seconds: 3); void connect(String userId, bool isReconnect) { id = userId; wsUrl = Uri.parse('ws://10.0.2.2:8000/ws/$id?is_reconnect=$isReconnect'); if (isReconnect) { socketStatus = SocketStatus.reconnecting; notifyListeners(); } // This doesn't blcok the programe whethe it connect the server or not // So heartBeat will be executre straightly channel = WebSocketChannel.connect(wsUrl); heartBeatInspect(); channel.stream.listen(onData, onError: onError, onDone: onDone); } void disconnect() { channel.sink.close(); wsUrl = Uri(); id = ''; socketStatus = SocketStatus.closed; notifyListeners(); heartBeatTimer?.cancel(); serverTimer?.cancel(); reconnectTimer?.cancel(); heartBeatTimer = null; serverTimer = null; reconnectTimer = null; reconnectTimes = 0; } void onData(jsonData) { // If socket can receive msg, that means connection is estabilished socketStatus = SocketStatus.connected; notifyListeners(); print('websocket connected <$channel>'); if (reconnectTimer != null) { reconnectTimer!.cancel(); reconnectTimer = null; reconnectTimes = 0; } heartBeatInspect(); Map data = json.decode(jsonData); switch (data['event']) { case 'friend-chat-msg': receiveFriendMsg(data, true); case 'apply-friend': receiveApplyFriend(data); case 'friend-added': receiveFriendAdded(data); case 'friend-deleted': receiveFriendDeleted(data); case 'chat-image': receiveChatImages(data); case 'group-chat-creation': receiveGroupChatCreation(data); case 'group-chat-msg': receiveGroupChatMsg(data, true); } } // This will be trigger while server or client close the connection // for example server is restarting void onDone() { print('websocket disconnected <$channel>'); print('22222222222222222222'); if (socketStatus == SocketStatus.closed) { // Client close the connection return; } if (socketStatus == SocketStatus.connected) { // Server close the connection socketStatus = SocketStatus.reconnecting; notifyListeners(); print(111111111111111); reconnectTimes++; reconnect(); } if (reconnectTimes >= reconnectCount) { socketStatus = SocketStatus.error; reconnectTimes = 0; } else { socketStatus = SocketStatus.reconnecting; print('3333333333333333333'); notifyListeners(); } } // This will be trigger while server exactly shutdown void onError(Object error, StackTrace st) { print('Websocket connect occurs error: <$error>'); // print(st); if (reconnectTimes >= reconnectCount) { socketStatus = SocketStatus.error; notifyListeners(); channel.sink.close(); if (heartBeatTimer != null) { heartBeatTimer!.cancel(); heartBeatTimer = null; } } else { print('${reconnectTimes}th reconnection'); reconnect(); } } void heartBeatInspect() { print('start heartbeat inspect......'); if (heartBeatTimer != null) { heartBeatTimer!.cancel(); heartBeatTimer = null; } if (serverTimer != null) { serverTimer!.cancel(); serverTimer = null; } heartBeatTimer = Timer(heartBeatTimeout, () { channel.sink.add(json.encode({'event': 'ping'})); serverTimer = Timer(heartBeatTimeout, () { // This will trigger the onDone callback channel.sink.close(status.internalServerError); socketStatus = SocketStatus.reconnecting; notifyListeners(); }); }); } void reconnect() { if (heartBeatTimer != null) { heartBeatTimer!.cancel(); heartBeatTimer = null; } if (serverTimer != null) { serverTimer!.cancel(); serverTimer = null; } if (reconnectTimer != null) { reconnectTimer!.cancel(); reconnectTimer = null; } reconnectTimer = Timer(reconnectTimeout, () { if (reconnectTimes < reconnectCount) { print('websocket reconnecting......'); reconnectTimes++; connect(id, true); } else { print('reconnection times exceed the max times......'); // If it is still disconnection after reconnect 30 times, set the socket // status to error, means the network is bad, and stop reconnecting. socketStatus = SocketStatus.error; notifyListeners(); channel.sink.close(); } }); } } void receiveFriendMsg(Map msg, bool isShowNotification) async { print('=================收到了好友信息事件=================='); print(msg); print('======================================='); String senderId = msg['senderId'] as String; Box chatSettingBox = Hive.box('chat_setting'); Box messageTBox = await HiveDatabase.openMessageBox(senderId); ChatSetting? chatSetting = chatSettingBox.get(senderId); DateTime dateTime = DateTime.parse(msg['dateTime'] as String); if (chatSetting == null) { chatSettingBox.put( senderId, ChatSetting( senderId, 0, false, true, false, dateTime, 1, ), ); } else { chatSetting.isOpen = true; chatSetting.latestDateTime = dateTime; chatSetting.unreadCount++; chatSettingBox.put(senderId, chatSetting); } List attachments = List.from(msg['attachments']); final DateTime now = DateTime.parse(msg['dateTime'] as String); messageTBox.add( MessageT( msg['msgId'] as int, senderId, msg['text'], msg['type'], now, msg['isShowTime'], attachments, ), ); if (!isShowNotification) { return; } String name = getIt.get().friends[senderId]!.friendRemark.isEmpty ? getIt.get().friends[senderId]!.nickname : getIt.get().friends[senderId]!.friendRemark; String routeName = getIt.get().currentPathName; String avatar = getIt.get().friends[senderId]!.avatar; if (!getIt.get().isVisible) { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, ); } else if (routeName == 'Message') { if (!getIt.get().query.containsValue(senderId)) { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, ); } } else if (routeName != 'Chat') { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, ); } } void receiveApplyFriend(Map msg) { print('=================收到了申请好友事件=================='); print(msg); print('======================================='); getIt.get().addJson(msg); } void receiveFriendAdded(Map msg) { print('=================收到了申请好友通过事件=================='); print(msg); print('======================================='); getIt.get().addFriend(msg['friendId'], msg['setting']); getIt .get() .addFriendAccountProfile(msg['friendId'], msg['accountProfile']); } void receiveFriendDeleted(Map msg) { print('=================收到了解除好友事件=================='); print(msg); print('======================================='); getIt.get().removeFriend(msg['friendId']); getIt.get().removeFriend(msg['friendId']); } void receiveChatImages(Map msg) async { print('=================收到了聊天图片事件=================='); print(msg); print('======================================='); String chatImageDir = getIt.get().baseImageDir; File file = File('$chatImageDir/${msg['filename']}'); if (await file.exists()) { return; } else { await file.create(recursive: true); await file.writeAsBytes(List.from(msg['bytes'])); } } void receiveGroupChatCreation(Map msg) { print('=================收到了群聊邀请事件=================='); print(msg); print('======================================='); String groupChatId = msg['groupChat']['id']; getIt.get().addGroupChat(groupChatId); getIt.get().addGroupChatProfile(groupChatId, msg); } void receiveGroupChatMsg( Map msg, bool isShowNotification) async { print('=================收到了群聊信息事件=================='); print(msg); print('======================================='); String senderId = msg['senderId'] as String; String groupChatId = msg['groupChatId'] as String; Box chatSettingBox = Hive.box('chat_setting'); Box messageTBox = await HiveDatabase.openMessageBox(groupChatId); ChatSetting? chatSetting = chatSettingBox.get(groupChatId); DateTime dateTime = DateTime.parse(msg['dateTime'] as String); getIt.get().addGroupChatMemberProfile( groupChatId, msg['senderId'], { 'avatar': msg['avatar'], 'nickname': msg['nickname'], 'remarkInGroupChat': msg['remarkInGroupChat'], }, ); if (chatSetting == null) { chatSettingBox.put( groupChatId, ChatSetting( groupChatId, 1, false, true, false, dateTime, 1, ), ); } else { chatSetting.isOpen = true; chatSetting.latestDateTime = dateTime; chatSetting.unreadCount++; chatSettingBox.put(groupChatId, chatSetting); } List attachments = List.from(msg['attachments']); final DateTime now = DateTime.parse(msg['dateTime'] as String); messageTBox.add( MessageT( msg['msgId'] as int, senderId, msg['text'], msg['type'], now, msg['isShowTime'], attachments, ), ); if (!isShowNotification) { return; } String avatar = getIt.get().groupChats[groupChatId]!.avatar; late String name; if (getIt.get().friends.containsKey(senderId)) { if (getIt.get().friends[senderId]!.friendRemark.isNotEmpty) { name = getIt.get().friends[senderId]!.friendRemark; } else if ((msg['remarkInGroupChat'] as String).isNotEmpty) { name = msg['remarkInGroupChat']; } else { name = msg['nickname']; } } else { name = (msg['remarkInGroupChat'] as String).isNotEmpty ? msg['remarkInGroupChat'] : msg['nickname']; } String routeName = getIt.get().currentPathName; if (!getIt.get().isVisible) { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, groupChatId: groupChatId, ); } else if (routeName == 'Message') { if (!getIt.get().query.containsValue(senderId)) { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, groupChatId: groupChatId, ); } } else if (routeName != 'Chat') { NotificationAPI.showMessageNotifications( senderId: senderId, name: name, avatar: avatar, text: msg['text'] as String, groupChatId: groupChatId, ); } }