diff --git a/dev/AppLifecycle/AppInstance.cpp b/dev/AppLifecycle/AppInstance.cpp index 2b0d38ee0c..f27dbbcff0 100644 --- a/dev/AppLifecycle/AppInstance.cpp +++ b/dev/AppLifecycle/AppInstance.cpp @@ -181,6 +181,121 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation m_redirectionArgs.Enqueue(id); } + // Checks if a file activation is a duplicate of a recent activation + // This is used to prevent duplicate activations when multiple files are opened at once + // When a user selects multiple files in Explorer and opens them, Windows may send multiple + // file activation events in quick succession, each containing the same set of files. + bool AppInstance::IsRecentFileActivation(Microsoft::Windows::AppLifecycle::AppActivationArguments const& args) + { + // Only check for duplicate file activations + if (args.Kind() != ExtendedActivationKind::File) + { + return false; + } + + try + { + auto data = args.Data().try_as(); + if (!data) + { + return false; + } + + // Get file paths from the activation + std::set newFilePaths; + auto files = data.Files(); + for (uint32_t i = 0; i < files.Size(); i++) + { + newFilePaths.insert(files.GetAt(i).Path()); + } + + // If no files, not a duplicate + if (newFilePaths.empty()) + { + return false; + } + + auto now = std::chrono::system_clock::now(); + + // Remove old activation records (older than 1 second) + // We only want to detect duplicates that happen in quick succession, + // which is the typical case for multi-file selection in Explorer + auto releaseOnExit = m_dataMutex.acquire(); + auto it = m_recentFileActivations.begin(); + while (it != m_recentFileActivations.end()) + { + if (now - it->timestamp > std::chrono::seconds(1)) + { + it = m_recentFileActivations.erase(it); + } + else + { + ++it; + } + } + + // Check if this set of files matches a recent activation + for (const auto& recent : m_recentFileActivations) + { + // If file sets are the same, this is a duplicate activation + if (recent.filePaths == newFilePaths) + { + return true; + } + } + } + catch (...) + { + // If any exception occurs, assume it's not a duplicate + return false; + } + + return false; + } + + // Records a file activation to prevent future duplicates + void AppInstance::RecordFileActivation(Microsoft::Windows::AppLifecycle::AppActivationArguments const& args) + { + if (args.Kind() != ExtendedActivationKind::File) + { + return; + } + + try + { + auto data = args.Data().try_as(); + if (!data) + { + return; + } + + // Get file paths from the activation + std::set filePaths; + auto files = data.Files(); + for (uint32_t i = 0; i < files.Size(); i++) + { + filePaths.insert(files.GetAt(i).Path()); + } + + // If no files, don't record + if (filePaths.empty()) + { + return; + } + + // Add to recent activations + auto releaseOnExit = m_dataMutex.acquire(); + m_recentFileActivations.push_back({ + std::chrono::system_clock::now(), + std::move(filePaths) + }); + } + catch (...) + { + // Ignore errors in recording + } + } + void AppInstance::ProcessRedirectionRequests() { m_innerActivated.ResetEvent(); @@ -197,6 +312,23 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation request.Open(name); auto args = request.UnmarshalArguments(); + // Skip this activation if it's a duplicate file activation that was recently processed + // This prevents multiple activations when a user selects multiple files in Explorer + // and opens them all at once, which can cause Windows to send multiple activation events + if (IsRecentFileActivation(args)) + { + std::wstring eventName = name + c_activatedEventNameSuffix; + wil::unique_event cleanupEvent; + if (cleanupEvent.try_open(eventName.c_str())) + { + cleanupEvent.SetEvent(); + } + continue; + } + + // Record this file activation to prevent duplicates + RecordFileActivation(args); + // Notify the app that the redirection request is here. m_activatedEvent(*this, args); diff --git a/dev/AppLifecycle/AppInstance.h b/dev/AppLifecycle/AppInstance.h index 57eb83ea7f..a631f3c551 100644 --- a/dev/AppLifecycle/AppInstance.h +++ b/dev/AppLifecycle/AppInstance.h @@ -8,6 +8,9 @@ #include "RedirectionRequest.h" #include "SharedProcessList.h" #include "RedirectionRequestQueue.h" +#include +#include +#include namespace winrt::Microsoft::Windows::AppLifecycle::implementation { @@ -53,6 +56,8 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation Microsoft::Windows::AppLifecycle::AppInstance FindForKey(std::wstring const& key); void EnqueueRedirectionRequestId(GUID id); GUID DequeueRedirectionRequestId(); + bool IsRecentFileActivation(Microsoft::Windows::AppLifecycle::AppActivationArguments const& args); + void RecordFileActivation(Microsoft::Windows::AppLifecycle::AppActivationArguments const& args); // Named object prefixes used to scope. std::wstring m_moduleName; @@ -83,6 +88,17 @@ namespace winrt::Microsoft::Windows::AppLifecycle::implementation SharedProcessList m_instances; RedirectionRequestQueue m_redirectionArgs; + + // Tracking structure for recent file activations to prevent duplicates + // This is used to prevent multiple activations when a user selects multiple files + // in Explorer and opens them together, which can cause Windows to send multiple + // activation events, each containing the same set of files + struct RecentFileActivation + { + std::chrono::system_clock::time_point timestamp; + std::set filePaths; + }; + std::vector m_recentFileActivations; }; }