From 6e09de53166d44c7d685b05af920e5c02f98ad41 Mon Sep 17 00:00:00 2001 From: Solin Date: Tue, 20 Jan 2026 12:10:06 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E4=BD=93=E7=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ActivityMonitor.cs | 52 ++----- MainForm.cs | 5 +- NativeMethods.cs | 335 --------------------------------------------- TimerApp.csproj | 2 +- 4 files changed, 15 insertions(+), 379 deletions(-) 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