first commit
This commit is contained in:
120
Services/NotificationService.cs
Normal file
120
Services/NotificationService.cs
Normal file
@ -0,0 +1,120 @@
|
||||
namespace HeartRateMonitorAndroid.Services
|
||||
{
|
||||
// 跨平台通知服务
|
||||
public static class NotificationService
|
||||
{
|
||||
// 常量定义
|
||||
private const string CHANNEL_ID = "HeartRateMonitorChannel";
|
||||
private const int NOTIFICATION_ID = 100;
|
||||
|
||||
// 初始化通知服务
|
||||
public static void Initialize()
|
||||
{
|
||||
// 根据平台初始化
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CreateNotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"心率监测",
|
||||
"显示实时心率数据");
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
// 请求iOS通知权限
|
||||
Platforms.iOS.IosNotificationHelper.RequestNotificationPermission().ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// 显示心率通知
|
||||
public static void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
string title = "心率监测";
|
||||
string content = $"当前心率: {currentHeartRate} bpm 平均: {avgHeartRate:0} bpm";
|
||||
string bigText = $"当前心率: {currentHeartRate} bpm\n监测时长: {duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}\n最低: {minHeartRate} bpm | 最高: {maxHeartRate} bpm";
|
||||
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowBigTextNotification(
|
||||
CHANNEL_ID,
|
||||
NOTIFICATION_ID,
|
||||
title,
|
||||
content,
|
||||
bigText,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
true);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// 取消通知
|
||||
public static void CancelNotification()
|
||||
{
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CancelNotification(NOTIFICATION_ID);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.CancelAllNotifications();
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.CancelNotification();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重连通知
|
||||
public static void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
const int RECONNECTION_NOTIFICATION_ID = 101; // 使用不同的ID,避免覆盖心率通知
|
||||
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowNormalNotification(
|
||||
CHANNEL_ID,
|
||||
RECONNECTION_NOTIFICATION_ID,
|
||||
title,
|
||||
message,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
false); // 不使用前台服务,只显示普通通知
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
294
Services/WebSocketService.cs
Normal file
294
Services/WebSocketService.cs
Normal file
@ -0,0 +1,294 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace HeartRateMonitorAndroid.Services;
|
||||
|
||||
public class WebSocketService
|
||||
{
|
||||
// 心率上报
|
||||
public class HeartRateWebSocketClient : IDisposable
|
||||
{
|
||||
private readonly string _serverUrl;
|
||||
private ClientWebSocket _webSocket;
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _isConnected = false;
|
||||
private int _reconnectDelayMs = 5000; // 初始重连延时5秒
|
||||
private readonly int _maxReconnectDelayMs = 60000; // 最大重连延时60秒
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _isReconnecting = false; // 是否正在重连
|
||||
private int _reconnectAttempts = 0; // 重连尝试次数
|
||||
|
||||
public HeartRateWebSocketClient(string serverUrl)
|
||||
{
|
||||
_serverUrl = serverUrl;
|
||||
_webSocket = new ClientWebSocket();
|
||||
|
||||
// 配置WebSocket客户端选项,增强后台运行稳定性
|
||||
_webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
|
||||
_webSocket.Options.SetBuffer(8192, 8192); // 增加缓冲区大小
|
||||
|
||||
// 在某些Android设备上,默认SubProtocol可能导致连接问题
|
||||
// _webSocket.Options.AddSubProtocol("json"); // 可以根据服务器要求添加子协议
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
if (_isConnected) return;
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"正在连接到 WebSocket 服务器: {_serverUrl}");
|
||||
await _webSocket.ConnectAsync(new Uri(_serverUrl), _cts.Token);
|
||||
|
||||
_isConnected = true;
|
||||
Console.WriteLine("已成功连接到 WebSocket 服务器");
|
||||
|
||||
// 连接成功,重置重连参数
|
||||
_reconnectAttempts = 0;
|
||||
_reconnectDelayMs = 5000; // 重置为初始值
|
||||
|
||||
// 启动接收消息的任务
|
||||
_ = ReceiveMessagesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"连接 WebSocket 服务器失败: {ex.Message}");
|
||||
if (!_isReconnecting) // 只有在不是重连过程中才触发重连
|
||||
{
|
||||
await ReconnectAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveMessagesAsync()
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
try
|
||||
{
|
||||
while (_webSocket.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cts.Token);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
_isConnected = false;
|
||||
await ReconnectAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
Console.WriteLine($"收到服务器消息: {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"接收消息时出错: {ex.Message}");
|
||||
_isConnected = false;
|
||||
await ReconnectAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReconnectAsync()
|
||||
{
|
||||
if (_cts.IsCancellationRequested) return;
|
||||
|
||||
// 检查是否已经在重连中,避免多次重连
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_isReconnecting)
|
||||
{
|
||||
Console.WriteLine("已经有重连任务在进行中,跳过此次重连请求");
|
||||
return;
|
||||
}
|
||||
_isReconnecting = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 增加重连次数
|
||||
_reconnectAttempts++;
|
||||
|
||||
// 使用指数退避策略增加等待时间
|
||||
// 每次重连失败后,等待时间翻倍,但不超过最大值
|
||||
_reconnectDelayMs = Math.Min(_reconnectDelayMs * 2, _maxReconnectDelayMs);
|
||||
|
||||
Console.WriteLine($"第{_reconnectAttempts}次重连尝试,等待{_reconnectDelayMs / 1000}秒...");
|
||||
|
||||
// 如果重连次数超过特定阈值,显示通知提醒用户
|
||||
if (_reconnectAttempts == 3 || _reconnectAttempts == 5 || _reconnectAttempts % 10 == 0)
|
||||
{
|
||||
await ShowReconnectionNotification();
|
||||
}
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_webSocket.State != WebSocketState.Open && _webSocket.State != WebSocketState.Connecting)
|
||||
{
|
||||
_webSocket.Dispose();
|
||||
_webSocket = new ClientWebSocket();
|
||||
|
||||
// 设置WebSocket选项以提高后台连接可靠性
|
||||
_webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
|
||||
_webSocket.Options.SetBuffer(8192, 8192); // 增加缓冲区大小
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(_reconnectDelayMs);
|
||||
await ConnectAsync();
|
||||
|
||||
// 连接成功,重置重连计数和延迟
|
||||
if (_isConnected)
|
||||
{
|
||||
_reconnectAttempts = 0;
|
||||
_reconnectDelayMs = 5000; // 重置为初始值
|
||||
Console.WriteLine("重连成功,重置重连参数");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"重连过程中发生错误: {ex.Message}");
|
||||
// 使用当前的延迟时间再次尝试
|
||||
await Task.Delay(_reconnectDelayMs);
|
||||
// 释放重连锁,允许下次重连
|
||||
lock (_lockObject) { _isReconnecting = false; }
|
||||
await ReconnectAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保重连锁被释放
|
||||
lock (_lockObject) { _isReconnecting = false; }
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重连通知
|
||||
private async Task ShowReconnectionNotification()
|
||||
{
|
||||
try
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
var title = "连接中断";
|
||||
var message = $"服务器连接已断开,正在尝试第{_reconnectAttempts}次重连。";
|
||||
|
||||
// 使用应用程序的通知服务显示通知
|
||||
HeartRateMonitorAndroid.Services.NotificationService.ShowReconnectionNotification(
|
||||
title,
|
||||
message,
|
||||
_reconnectAttempts);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"显示重连通知失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendHeartRateDataAsync(HeartRateData data)
|
||||
{
|
||||
// 检查WebSocket状态
|
||||
if (_webSocket.State != WebSocketState.Open || !_isConnected)
|
||||
{
|
||||
Console.WriteLine($"WebSocket未连接,当前状态: {_webSocket.State},尝试重新连接");
|
||||
_isConnected = false;
|
||||
|
||||
// 如果已经在重连过程中,不要再次尝试连接
|
||||
if (!_isReconnecting)
|
||||
{
|
||||
await ConnectAsync();
|
||||
}
|
||||
|
||||
if (!_isConnected)
|
||||
{
|
||||
Console.WriteLine("重连失败,无法发送数据");
|
||||
|
||||
// 如果重连次数超过阈值,显示连接失败通知
|
||||
if (_reconnectAttempts >= 3 && !_isReconnecting)
|
||||
{
|
||||
await ShowReconnectionNotification();
|
||||
// 触发重连
|
||||
await ReconnectAsync();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data);
|
||||
var buffer = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
// 设置发送超时
|
||||
var sendCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
|
||||
sendCts.CancelAfter(TimeSpan.FromSeconds(5)); // 5秒超时
|
||||
|
||||
await _webSocket.SendAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
WebSocketMessageType.Text,
|
||||
true,
|
||||
sendCts.Token);
|
||||
|
||||
//Console.WriteLine($"成功发送心率数据: {data.HeartRate} bpm");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("发送数据超时");
|
||||
_isConnected = false;
|
||||
await ReconnectAsync();
|
||||
}
|
||||
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
|
||||
{
|
||||
Console.WriteLine("WebSocket连接已关闭,尝试重新连接");
|
||||
_isConnected = false;
|
||||
await ReconnectAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"发送心率数据失败: {ex.Message}, 类型: {ex.GetType().Name}");
|
||||
_isConnected = false;
|
||||
await ReconnectAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
if (_webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "客户端关闭", CancellationToken.None)
|
||||
.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
_webSocket.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"关闭WebSocket客户端时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
public class HeartRateData
|
||||
{
|
||||
public int HeartRate { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string DeviceName { get; set; }
|
||||
public string Token
|
||||
{
|
||||
get
|
||||
{
|
||||
using var stream = FileSystem.OpenAppPackageFileAsync("token.txt").Result;
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var contents = reader.ReadToEnd();
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user