Files
TimerApp/ActivityMonitor.cs

318 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Windows.Forms;
using Microsoft.Win32;
namespace TimerApp
{
public enum MonitorState
{
Idle,
Working,
Resting
}
public sealed class ActivityMonitor : IDisposable
{
private CancellationTokenSource? _cts;
private readonly object _lock = new object();
private TimeSpan _accumulatedWorkTime;
private int _restElapsedSeconds;
private bool _isPaused;
private bool _disposed;
// 状态检测缓存
private int _checkTickCounter;
private const int CheckIntervalTicks = 3; // 每3秒检查一次空闲/媒体状态
private long _cachedIdleMs;
private bool _cachedMediaPlaying;
// 配置 (默认值)
public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20);
public TimeSpan RestDuration { get; set; } = TimeSpan.FromMinutes(1);
public TimeSpan IdleThreshold { get; set; } = TimeSpan.FromSeconds(30);
public MonitorState CurrentState { get; private set; } = MonitorState.Idle;
public bool IsPaused
{
get { lock (_lock) return _isPaused; }
private set { lock (_lock) _isPaused = value; }
}
// 事件
public event EventHandler<TimeSpan>? WorkProgressChanged; // 剩余工作时间
public event EventHandler<TimeSpan>? RestProgressChanged; // 剩余休息时间
public event EventHandler? RestStarted;
public event EventHandler? RestEnded;
public event EventHandler? StateChanged;
public ActivityMonitor()
{
SystemEvents.PowerModeChanged += OnPowerModeChanged;
}
public void Start()
{
lock (_lock)
{
StopInternal();
_cts = new CancellationTokenSource();
var token = _cts.Token;
// 启动时立即触发一次检测
_checkTickCounter = CheckIntervalTicks;
ResetWork();
Task.Run(() => MonitorLoop(token), token);
}
}
public void Stop()
{
lock (_lock)
{
StopInternal();
}
}
private void StopInternal()
{
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
}
private void ResetWork()
{
// Must be called within lock
_accumulatedWorkTime = TimeSpan.Zero;
ChangeState(MonitorState.Idle);
}
private void ChangeState(MonitorState newState)
{
// Must be called within lock
if (CurrentState != newState)
{
CurrentState = newState;
StateChanged?.Invoke(this, EventArgs.Empty);
}
}
private async Task MonitorLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(1000, token);
OnTick();
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Logger.LogError("Error in ActivityMonitor MonitorLoop", ex);
}
}
}
private void OnTick()
{
lock (_lock)
{
// 如果处于暂停状态,不处理计时逻辑
if (_isPaused)
{
return;
}
long idleMs;
TimeSpan idleTime;
// 优化降低系统API调用频率
// 每 CheckIntervalTicks (3) 秒更新一次状态
_checkTickCounter++;
if (_checkTickCounter >= CheckIntervalTicks)
{
_checkTickCounter = 0;
_cachedIdleMs = NativeMethods.GetIdleTime();
_cachedMediaPlaying = NativeMethods.IsMediaPlaying();
}
idleMs = _cachedIdleMs;
idleTime = TimeSpan.FromMilliseconds(idleMs);
if (CurrentState == MonitorState.Resting)
{
// 休息模式逻辑
// 使用计数器而不是时间差,避免秒数跳变
_restElapsedSeconds++;
int totalRestSeconds = (int)RestDuration.TotalSeconds;
int remainingSeconds = totalRestSeconds - _restElapsedSeconds;
if (remainingSeconds <= 0)
{
// 休息结束
RestEnded?.Invoke(this, EventArgs.Empty);
ResetWork(); // 重新开始工作周期
}
else
{
TimeSpan remainingRest = TimeSpan.FromSeconds(remainingSeconds);
RestProgressChanged?.Invoke(this, remainingRest);
}
}
else
{
// 使用缓存的媒体播放状态
bool isMediaPlaying = _cachedMediaPlaying;
// 工作/空闲模式逻辑
if (idleTime > IdleThreshold || isMediaPlaying)
{
// 用户离开了或正在播放视频
if (CurrentState == MonitorState.Working)
{
// 如果空闲时间超过阈值,状态变为空闲
ChangeState(MonitorState.Idle);
}
// 如果在 Idle 状态,且空闲时间非常长(比如超过了休息时间),
// 重置工作计时器
if (idleTime > RestDuration)
{
_accumulatedWorkTime = TimeSpan.Zero;
}
// 如果正在播放视频,不累加工作时间,但保持当前状态
if (isMediaPlaying && CurrentState == MonitorState.Working)
{
TimeSpan remainingWork = WorkDuration - _accumulatedWorkTime;
WorkProgressChanged?.Invoke(this, remainingWork);
}
}
else
{
// 用户在活动且没有播放视频
if (CurrentState == MonitorState.Idle)
{
// 从空闲变为工作
ChangeState(MonitorState.Working);
}
// 累加工作时间
_accumulatedWorkTime += TimeSpan.FromSeconds(1);
// 检查是否达到工作时长
TimeSpan remainingWork = WorkDuration - _accumulatedWorkTime;
if (remainingWork <= TimeSpan.Zero)
{
// 触发休息
_restElapsedSeconds = 0; // 重置休息计数器
ChangeState(MonitorState.Resting);
RestStarted?.Invoke(this, EventArgs.Empty);
}
else
{
WorkProgressChanged?.Invoke(this, remainingWork);
}
}
}
}
}
public void RefreshStatus()
{
lock (_lock)
{
if (CurrentState == MonitorState.Working)
{
TimeSpan remaining = WorkDuration - _accumulatedWorkTime;
WorkProgressChanged?.Invoke(this, remaining);
}
else if (CurrentState == MonitorState.Resting)
{
int totalRestSeconds = (int)RestDuration.TotalSeconds;
int remainingSeconds = totalRestSeconds - _restElapsedSeconds;
if (remainingSeconds < 0) remainingSeconds = 0;
TimeSpan remaining = TimeSpan.FromSeconds(remainingSeconds);
RestProgressChanged?.Invoke(this, remaining);
}
}
}
public void Restart()
{
lock (_lock)
{
_accumulatedWorkTime = TimeSpan.Zero;
_isPaused = false;
// Ensure task is running
if (_cts == null || _cts.IsCancellationRequested)
{
// Re-start if stopped (though Restart implies it's running)
// Usually Start() calls ResetWork, so we just reset here
}
// Force state to Working since user manually restarted
ChangeState(MonitorState.Working);
// Immediately refresh UI to show full duration
RefreshStatus();
}
}
public void Pause()
{
lock (_lock)
{
if (!_isPaused && CurrentState != MonitorState.Idle)
{
_isPaused = true;
StateChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public void Resume()
{
lock (_lock)
{
if (_isPaused)
{
_isPaused = false;
StateChanged?.Invoke(this, EventArgs.Empty);
RefreshStatus();
}
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
Stop();
}
private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
if (e.Mode == PowerModes.Resume)
{
// 系统唤醒时,强制重置媒体播放检测状态,
// 避免因检测线程挂起导致一直误报“正在播放”而无法进入工作状态。
NativeMethods.InvalidateMediaCache();
}
}
}
}