diff --git a/ActivityMonitor.cs b/ActivityMonitor.cs
index 887a6cc..a221958 100644
--- a/ActivityMonitor.cs
+++ b/ActivityMonitor.cs
@@ -22,9 +22,8 @@ namespace TimerApp
// 状态检测缓存
private int _checkTickCounter;
- private const int CheckIntervalTicks = 3; // 每3秒检查一次空闲/媒体状态
+ private const int CheckIntervalTicks = 3; // 每3秒检查一次空闲状态
private long _cachedIdleMs;
- private bool _cachedMediaPlaying;
// 配置 (默认值)
public TimeSpan WorkDuration { get; set; } = TimeSpan.FromMinutes(20);
@@ -47,7 +46,6 @@ namespace TimerApp
public ActivityMonitor()
{
- SystemEvents.PowerModeChanged += OnPowerModeChanged;
}
public void Start()
@@ -126,7 +124,7 @@ namespace TimerApp
{
// 在锁外执行耗时的系统检测
long idleMs = 0;
- bool? newMediaPlaying = null;
+ bool shouldUpdate = false;
lock (_lock)
{
@@ -139,27 +137,25 @@ namespace TimerApp
{
_checkTickCounter = 0;
// 需要更新,但在锁外进行
- newMediaPlaying = true;
+ shouldUpdate = true;
}
// 如果不需要更新,直接使用缓存值
- if (newMediaPlaying == null)
+ if (!shouldUpdate)
{
idleMs = _cachedIdleMs;
}
}
// 在锁外执行实际的检测
- if (newMediaPlaying == true)
+ if (shouldUpdate)
{
idleMs = NativeMethods.GetIdleTime();
- bool playing = NativeMethods.IsMediaPlaying();
// 更新缓存
lock (_lock)
{
_cachedIdleMs = idleMs;
- _cachedMediaPlaying = playing;
}
}
@@ -170,7 +166,6 @@ namespace TimerApp
// 使用(可能是新更新的)缓存值
idleMs = _cachedIdleMs;
- bool isMediaPlaying = _cachedMediaPlaying;
TimeSpan idleTime = TimeSpan.FromMilliseconds(idleMs);
if (CurrentState == MonitorState.Resting)
@@ -196,12 +191,13 @@ namespace TimerApp
else
{
// 工作/空闲模式逻辑
- if (idleTime > IdleThreshold || isMediaPlaying)
+ bool isUserInactive = idleTime > IdleThreshold;
+
+ if (isUserInactive)
{
- // 用户离开了或正在播放视频
+ // 用户确实离开了 -> 进入空闲状态
if (CurrentState == MonitorState.Working)
{
- // 如果空闲时间超过阈值,状态变为空闲
ChangeState(MonitorState.Idle);
}
@@ -211,24 +207,17 @@ namespace TimerApp
{
_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);
// 检查是否达到工作时长
@@ -277,13 +266,6 @@ namespace TimerApp
_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);
@@ -322,18 +304,10 @@ namespace TimerApp
if (_disposed) return;
_disposed = true;
- SystemEvents.PowerModeChanged -= OnPowerModeChanged;
+ // SystemEvents.PowerModeChanged -= OnPowerModeChanged;
Stop();
+ GC.SuppressFinalize(this);
}
- private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
- {
- if (e.Mode == PowerModes.Resume)
- {
- // 系统唤醒时,强制重置媒体播放检测状态,
- // 避免因检测线程挂起导致一直误报“正在播放”而无法进入工作状态。
- NativeMethods.InvalidateMediaCache();
- }
- }
}
}
diff --git a/MainForm.cs b/MainForm.cs
index 4467ac6..e12cb2a 100644
--- a/MainForm.cs
+++ b/MainForm.cs
@@ -25,10 +25,6 @@ namespace TimerApp
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
- [DllImport("user32.dll")]
- static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight);
- [DllImport("user32.dll")]
- static extern bool ShowCaret(IntPtr hWnd);
private const int WM_NCLBUTTONDOWN = 0xA1;
private const int HT_CAPTION = 0x2;
@@ -611,6 +607,7 @@ namespace TimerApp
private void toolStripMenuItemExit_Click(object sender, EventArgs e)
{
_monitor.Stop();
+ _monitor.Dispose();
notifyIcon1.Visible = false;
notifyIcon1.Dispose();
Application.Exit();
diff --git a/NativeMethods.cs b/NativeMethods.cs
index d6d46f3..3271a77 100644
--- a/NativeMethods.cs
+++ b/NativeMethods.cs
@@ -2,7 +2,6 @@ using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using Windows.Media.Control;
namespace TimerApp
{
@@ -47,346 +46,12 @@ namespace TimerApp
return 0;
}
- // ==========================================
- // Media Detection Logic (Refactored)
- // ==========================================
-
- private static volatile bool _isMediaPlayingCaught;
- private static Thread? _mediaMonitorThread;
- private static readonly CancellationTokenSource _cts = new CancellationTokenSource();
- private static readonly AutoResetEvent _checkSignal = new AutoResetEvent(false);
-
static NativeMethods()
{
- // 在静态构造函数中启动后台监控线程
- StartMediaMonitor();
}
- ///
- /// 检测是否有媒体正在播放(视频或音频)
- /// 直接返回缓存的状态,无阻塞
- ///
- public static bool IsMediaPlaying()
- {
- return _isMediaPlayingCaught;
- }
-
- ///
- /// 强制立即进行一次检测
- ///
- public static void InvalidateMediaCache()
- {
- // 唤醒后台线程立即检测
- _checkSignal.Set();
- }
-
- ///
- /// 应用退出时清理资源 (可选调用)
- ///
public static void Shutdown()
{
- _cts.Cancel();
- _checkSignal.Set(); // Wake up to exit
- }
-
- private static void StartMediaMonitor()
- {
- if (_mediaMonitorThread != null) return;
-
- _mediaMonitorThread = new Thread(MediaMonitorLoop)
- {
- IsBackground = true,
- Name = "TimerApp_MediaMonitor",
- Priority = ThreadPriority.BelowNormal
- };
- _mediaMonitorThread.SetApartmentState(ApartmentState.STA); // COM requirement
- _mediaMonitorThread.Start();
- }
-
- private static void MediaMonitorLoop()
- {
- while (!_cts.IsCancellationRequested)
- {
- try
- {
- bool isPlaying = false;
-
- // 1. Check System Media Transport Controls (WinRT)
- // 需要在 STA 线程中小心处理 Task
- try
- {
- // 使用 Task.Run 确保在一个干净的上下文执行异步任务,然后同步等待结果
- // 注意:因为我们本身就在 STA 线程,直接 .Result 可能会有风险,
- // 但对于 GSMTC API,只要没有 SynchronizationContext 绑定到此线程通常是安全的。
- // 这里为了稳妥,我们捕获任何异常。
- isPlaying = CheckSystemMediaTransportControls();
- }
- catch
- {
- // Ignored
- }
-
- // 2. Check Legacy Audio Session API (COM)
- if (!isPlaying)
- {
- try
- {
- isPlaying = TryIsAudioPlaying();
- }
- catch
- {
- // Ignored
- }
- }
-
- // Update volatile cache
- _isMediaPlayingCaught = isPlaying;
-
- // Wait for next check (default 1s, or wake up immediately on Invalidate)
- _checkSignal.WaitOne(1000);
- }
- catch
- {
- // Prevent thread crash
- Thread.Sleep(1000);
- }
- }
- }
-
- private static bool CheckSystemMediaTransportControls()
- {
- // 同步等待异步方法,因为我们在专用线程上,且没有 UI SyncContext,这是安全的
- return CheckSystemMediaTransportControlsAsync().GetAwaiter().GetResult();
- }
-
- private static async Task CheckSystemMediaTransportControlsAsync()
- {
- try
- {
- // RequestAsync 可能在某些系统上较慢
- var manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
-
- var current = manager.GetCurrentSession();
- if (IsPlaying(current)) return true;
-
- foreach (var session in manager.GetSessions())
- {
- if (IsPlaying(session)) return true;
- }
-
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- private static bool IsPlaying(GlobalSystemMediaTransportControlsSession? session)
- {
- if (session == null) return false;
- try
- {
- var info = session.GetPlaybackInfo();
- return info != null && info.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing;
- }
- catch
- {
- return false;
- }
- }
-
- // ==========================================
- // COM Audio Detection
- // ==========================================
-
- private static bool TryIsAudioPlaying()
- {
- return TryIsAudioPlaying(ERole.eMultimedia) ||
- TryIsAudioPlaying(ERole.eConsole) ||
- TryIsAudioPlaying(ERole.eCommunications);
- }
-
- private static bool TryIsAudioPlaying(ERole role)
- {
- object? deviceEnumeratorObj = null;
- IMMDeviceEnumerator? deviceEnumerator = null;
- IMMDevice? device = null;
- object? sessionManagerObj = null;
- IAudioSessionManager2? sessionManager = null;
- IAudioSessionEnumerator? sessionEnumerator = null;
-
- try
- {
- Type? enumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator);
- if (enumeratorType is null) return false;
-
- deviceEnumeratorObj = Activator.CreateInstance(enumeratorType);
- if (deviceEnumeratorObj is null) return false;
- deviceEnumerator = (IMMDeviceEnumerator)deviceEnumeratorObj;
-
- // GetDefaultAudioEndpoint can fail if no audio device is present
- int hr = deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, role, out device);
- if (hr != 0) return false; // S_OK = 0
-
- Guid iid = typeof(IAudioSessionManager2).GUID;
- hr = device.Activate(ref iid, CLSCTX.CLSCTX_ALL, IntPtr.Zero, out sessionManagerObj);
- if (hr != 0 || sessionManagerObj is null) return false;
-
- sessionManager = (IAudioSessionManager2)sessionManagerObj;
-
- hr = sessionManager.GetSessionEnumerator(out sessionEnumerator);
- if (hr != 0) return false;
-
- hr = sessionEnumerator.GetCount(out int count);
- if (hr != 0) return false;
-
- for (int i = 0; i < count; i++)
- {
- IAudioSessionControl? sessionControl = null;
- try
- {
- hr = sessionEnumerator.GetSession(i, out sessionControl);
- if (hr != 0 || sessionControl is null) continue;
-
- hr = sessionControl.GetState(out AudioSessionState state);
- if (hr != 0) continue;
-
- if (state == AudioSessionState.Active)
- {
- if (sessionControl is IAudioMeterInformation meter)
- {
- hr = meter.GetPeakValue(out float peak);
- if (hr == 0 && peak > 0.000001f) // Slightly relaxed threshold
- return true;
- }
- else
- {
- // Cannot check peak, assume active means playing
- return true;
- }
- }
- }
- finally
- {
- if (sessionControl != null) Marshal.FinalReleaseComObject(sessionControl);
- }
- }
-
- return false;
- }
- catch
- {
- return false;
- }
- finally
- {
- if (sessionEnumerator != null) Marshal.FinalReleaseComObject(sessionEnumerator);
- if (sessionManager != null) Marshal.FinalReleaseComObject(sessionManager); // Corrected variable usage
- if (sessionManagerObj != null) Marshal.FinalReleaseComObject(sessionManagerObj);
- if (device != null) Marshal.FinalReleaseComObject(device);
- if (deviceEnumeratorObj != null) Marshal.FinalReleaseComObject(deviceEnumeratorObj);
- }
- }
-
- private static readonly Guid CLSID_MMDeviceEnumerator = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E");
-
- private enum EDataFlow
- {
- eRender = 0,
- eCapture = 1,
- eAll = 2
- }
-
- private enum ERole
- {
- eConsole = 0,
- eMultimedia = 1,
- eCommunications = 2
- }
-
- private enum AudioSessionState
- {
- Inactive = 0,
- Active = 1,
- Expired = 2
- }
-
- [Flags]
- private enum CLSCTX : uint
- {
- CLSCTX_INPROC_SERVER = 0x1,
- CLSCTX_INPROC_HANDLER = 0x2,
- CLSCTX_LOCAL_SERVER = 0x4,
- CLSCTX_REMOTE_SERVER = 0x10,
- CLSCTX_ALL = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER
- }
-
- [ComImport]
- [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IMMDeviceEnumerator
- {
- int NotImpl1();
- [PreserveSig]
- int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
- }
-
- [ComImport]
- [Guid("D666063F-1587-4E43-81F1-B948E807363F")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IMMDevice
- {
- [PreserveSig]
- int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
- }
-
- [ComImport]
- [Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IAudioSessionManager2
- {
- int NotImpl1();
- int NotImpl2();
- [PreserveSig]
- int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
- }
-
- [ComImport]
- [Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IAudioSessionEnumerator
- {
- [PreserveSig]
- int GetCount(out int sessionCount);
- [PreserveSig]
- int GetSession(int sessionCount, out IAudioSessionControl session);
- }
-
- [ComImport]
- [Guid("F4B1A599-7266-4319-A8CA-E70ACB11E8CD")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IAudioSessionControl
- {
- [PreserveSig]
- int GetState(out AudioSessionState state);
- int NotImpl1();
- int NotImpl2();
- int NotImpl3();
- int NotImpl4();
- int NotImpl5();
- int NotImpl6();
- int NotImpl7();
- int NotImpl8();
- }
-
- [ComImport]
- [Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064")]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IAudioMeterInformation
- {
- [PreserveSig]
- int GetPeakValue(out float peak);
}
}
}
diff --git a/TimerApp.csproj b/TimerApp.csproj
index 44f119f..a3de757 100644
--- a/TimerApp.csproj
+++ b/TimerApp.csproj
@@ -2,7 +2,7 @@
WinExe
- net9.0-windows10.0.19041.0
+ net9.0-windows
enable
true
enable