Merge remote-tracking branch 'origin/copilot/ensure-ble-hid-support-windows' into feature/openbikecontrol

# Conflicts:
#	.gitignore
This commit is contained in:
Jonas Bark
2025-12-01 18:23:14 +00:00
6 changed files with 239 additions and 9 deletions

View File

@@ -74,6 +74,8 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface {
LogicalKeyboardKey.mediaPlay: MediaKey.playPause,
LogicalKeyboardKey.mediaRewind: MediaKey.rewind,
LogicalKeyboardKey.mediaFastForward: MediaKey.fastForward,
LogicalKeyboardKey.audioVolumeUp: MediaKey.volumeUp,
LogicalKeyboardKey.audioVolumeDown: MediaKey.volumeDown,
};
/// The default handler to use if this platform doesn't need to implement any

View File

@@ -8,4 +8,10 @@ enum MediaKey {
/// The fast-forward media button
fastForward,
/// The volume up button
volumeUp,
/// The volume down button
volumeDown,
}

View File

@@ -1,6 +1,10 @@
# 0.0.2
- TBD
- Implement global media key detection using Windows RegisterHotKey API
- Add event channel support for media key events
- Media keys now work even when app is not focused
- Improved error handling for hotkey registration
- Added support for volume up and volume down hotkeys
# 0.0.1

View File

@@ -4,11 +4,60 @@
The windows implementation of `media_key_detector`.
## Features
This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) even when it's not the focused application.
### Supported Media Keys
- Play/Pause (VK_MEDIA_PLAY_PAUSE)
- Next Track (VK_MEDIA_NEXT_TRACK)
- Previous Track (VK_MEDIA_PREV_TRACK)
- Volume Up (VK_VOLUME_UP)
- Volume Down (VK_VOLUME_DOWN)
### Implementation Details
The plugin uses:
- `RegisterHotKey` Windows API for global hotkey registration
- Event channels for communicating media key events to Dart
- Window message handlers to process WM_HOTKEY messages
Hotkeys are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed.
## Usage
This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector`
normally. This package will be automatically included in your app when you do.
```dart
import 'package:media_key_detector/media_key_detector.dart';
// Enable media key detection
mediaKeyDetector.setIsPlaying(isPlaying: true);
// Listen for media key events
mediaKeyDetector.addListener((MediaKey key) {
switch (key) {
case MediaKey.playPause:
// Handle play/pause
break;
case MediaKey.fastForward:
// Handle next track
break;
case MediaKey.rewind:
// Handle previous track
break;
case MediaKey.volumeUp:
// Handle volume up
break;
case MediaKey.volumeDown:
// Handle volume down
break;
}
});
```
[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

View File

@@ -5,6 +5,7 @@ import 'package:media_key_detector_platform_interface/media_key_detector_platfor
/// The Windows implementation of [MediaKeyDetectorPlatform].
class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {
bool _isPlaying = false;
final _eventChannel = const EventChannel('media_key_detector_windows_events');
/// The method channel used to interact with the native platform.
@visibleForTesting
@@ -17,7 +18,16 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {
@override
void initialize() {
ServicesBinding.instance.keyboard.addHandler(defaultHandler);
_eventChannel.receiveBroadcastStream().listen((event) {
final keyIdx = event as int;
MediaKey? key;
if (keyIdx > -1 && keyIdx < MediaKey.values.length) {
key = MediaKey.values[keyIdx];
}
if (key != null) {
triggerListeners(key);
}
});
}
@override
@@ -27,11 +37,13 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {
@override
Future<bool> getIsPlaying() async {
return _isPlaying;
final isPlaying = await methodChannel.invokeMethod<bool>('getIsPlaying');
return isPlaying ?? _isPlaying;
}
@override
Future<void> setIsPlaying({required bool isPlaying}) async {
_isPlaying = isPlaying;
await methodChannel.invokeMethod<void>('setIsPlaying', <String, dynamic>{'isPlaying': isPlaying});
}
}

View File

@@ -6,19 +6,29 @@
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <flutter/event_channel.h>
#include <flutter/event_stream_handler_functions.h>
#include <map>
#include <memory>
#include <atomic>
namespace {
using flutter::EncodableValue;
// Hotkey IDs for media keys
constexpr int HOTKEY_PLAY_PAUSE = 1;
constexpr int HOTKEY_NEXT_TRACK = 2;
constexpr int HOTKEY_PREV_TRACK = 3;
constexpr int HOTKEY_VOLUME_UP = 4;
constexpr int HOTKEY_VOLUME_DOWN = 5;
class MediaKeyDetectorWindows : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
MediaKeyDetectorWindows();
MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar);
virtual ~MediaKeyDetectorWindows();
@@ -27,6 +37,21 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
// Register global hotkeys for media keys
void RegisterHotkeys();
// Unregister global hotkeys
void UnregisterHotkeys();
// Handle Windows messages
std::optional<LRESULT> HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
flutter::PluginRegistrarWindows *registrar_;
std::unique_ptr<flutter::EventSink<>> event_sink_;
std::atomic<bool> is_playing_{false};
int window_proc_id_ = -1;
bool hotkeys_registered_ = false;
};
// static
@@ -37,31 +62,163 @@ void MediaKeyDetectorWindows::RegisterWithRegistrar(
registrar->messenger(), "media_key_detector_windows",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<MediaKeyDetectorWindows>();
auto plugin = std::make_unique<MediaKeyDetectorWindows>(registrar);
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
// Set up event channel for media key events
auto event_channel =
std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
registrar->messenger(), "media_key_detector_windows_events",
&flutter::StandardMethodCodec::GetInstance());
auto event_handler = std::make_unique<flutter::StreamHandlerFunctions<>>(
[plugin_pointer = plugin.get()](
const flutter::EncodableValue* arguments,
std::unique_ptr<flutter::EventSink<>>&& events)
-> std::unique_ptr<flutter::StreamHandlerError<>> {
plugin_pointer->event_sink_ = std::move(events);
return nullptr;
},
[plugin_pointer = plugin.get()](const flutter::EncodableValue* arguments)
-> std::unique_ptr<flutter::StreamHandlerError<>> {
plugin_pointer->event_sink_ = nullptr;
return nullptr;
});
event_channel->SetStreamHandler(std::move(event_handler));
registrar->AddPlugin(std::move(plugin));
}
MediaKeyDetectorWindows::MediaKeyDetectorWindows() {}
MediaKeyDetectorWindows::MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar)
: registrar_(registrar) {
// Register a window procedure to handle hotkey messages
window_proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate(
[this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
return HandleWindowProc(hwnd, message, wparam, lparam);
});
}
MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {}
MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {
UnregisterHotkeys();
if (window_proc_id_ != -1) {
registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id_);
}
}
void MediaKeyDetectorWindows::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("getPlatformName") == 0) {
result->Success(EncodableValue("Windows"));
}
else {
} else if (method_call.method_name().compare("getIsPlaying") == 0) {
result->Success(EncodableValue(is_playing_.load()));
} else if (method_call.method_name().compare("setIsPlaying") == 0) {
const auto* arguments = std::get_if<flutter::EncodableMap>(method_call.arguments());
if (arguments) {
auto is_playing_it = arguments->find(EncodableValue("isPlaying"));
if (is_playing_it != arguments->end()) {
if (auto* is_playing = std::get_if<bool>(&is_playing_it->second)) {
is_playing_.store(*is_playing);
if (*is_playing) {
RegisterHotkeys();
} else {
UnregisterHotkeys();
}
result->Success();
return;
}
}
}
result->Error("INVALID_ARGUMENT", "isPlaying argument is required");
} else {
result->NotImplemented();
}
}
void MediaKeyDetectorWindows::RegisterHotkeys() {
if (hotkeys_registered_) {
return;
}
HWND hwnd = registrar_->GetView()->GetNativeWindow();
// Register global hotkeys for media keys
// MOD_NOREPEAT prevents the hotkey from repeating when held down
bool play_pause_ok = RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE);
bool next_ok = RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK);
bool prev_ok = RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK);
bool vol_up_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_UP, MOD_NOREPEAT, VK_VOLUME_UP);
bool vol_down_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_DOWN, MOD_NOREPEAT, VK_VOLUME_DOWN);
// If all registrations succeeded, mark as registered
// If any failed, unregister the successful ones to maintain consistent state
if (play_pause_ok && next_ok && prev_ok && vol_up_ok && vol_down_ok) {
hotkeys_registered_ = true;
} else {
// Clean up any successful registrations
if (play_pause_ok) UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE);
if (next_ok) UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK);
if (prev_ok) UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK);
if (vol_up_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP);
if (vol_down_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN);
}
}
void MediaKeyDetectorWindows::UnregisterHotkeys() {
if (!hotkeys_registered_) {
return;
}
HWND hwnd = registrar_->GetView()->GetNativeWindow();
UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE);
UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK);
UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK);
UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP);
UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN);
hotkeys_registered_ = false;
}
std::optional<LRESULT> MediaKeyDetectorWindows::HandleWindowProc(
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (message == WM_HOTKEY && event_sink_) {
int key_index = -1;
// Map hotkey ID to media key index
switch (wparam) {
case HOTKEY_PLAY_PAUSE:
key_index = 0; // MediaKey.playPause
break;
case HOTKEY_PREV_TRACK:
key_index = 1; // MediaKey.rewind
break;
case HOTKEY_NEXT_TRACK:
key_index = 2; // MediaKey.fastForward
break;
case HOTKEY_VOLUME_UP:
key_index = 3; // MediaKey.volumeUp
break;
case HOTKEY_VOLUME_DOWN:
key_index = 4; // MediaKey.volumeDown
break;
}
if (key_index >= 0) {
event_sink_->Success(EncodableValue(key_index));
}
return 0;
}
return std::nullopt;
}
} // namespace
void MediaKeyDetectorWindowsRegisterWithRegistrar(