Security: - Add flutter_secure_storage dependency for credential protection - Create SecureStorageService for encrypted credential storage - Migrate auth tokens, passwords, API tokens from SharedPreferences - Add automatic migration for existing users in main.dart Memory Leaks: - Fix audio session listener disposal in MassivAudioHandler - Store all 4 stream subscriptions and add dispose() method - Store connection state subscription in MusicAssistantProvider - Fix StreamController disposal in HardwareVolumeService Race Conditions: - Add reentrancy guard to selectPlayer() to prevent concurrent calls Code Quality: - Fix 7 silent exception handlers across codebase - Log errors with context instead of swallowing silently - Fix double opacity bug in player_controls.dart shuffle/repeat buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
2.9 KiB
Dart
91 lines
2.9 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/services.dart';
|
|
import 'debug_logger.dart';
|
|
|
|
/// Service to intercept hardware volume button presses
|
|
/// and route them to the selected Music Assistant player.
|
|
///
|
|
/// For MA players: intercept buttons and send volume commands via API
|
|
/// For builtin player: let native volume work (don't intercept)
|
|
class HardwareVolumeService {
|
|
static final HardwareVolumeService _instance = HardwareVolumeService._internal();
|
|
factory HardwareVolumeService() => _instance;
|
|
HardwareVolumeService._internal();
|
|
|
|
static const _channel = MethodChannel('com.musicassistant.music_assistant/volume_buttons');
|
|
final _logger = DebugLogger();
|
|
|
|
final _volumeUpController = StreamController<void>.broadcast();
|
|
final _volumeDownController = StreamController<void>.broadcast();
|
|
|
|
Stream<void> get onVolumeUp => _volumeUpController.stream;
|
|
Stream<void> get onVolumeDown => _volumeDownController.stream;
|
|
|
|
bool _isListening = false;
|
|
bool get isListening => _isListening;
|
|
|
|
bool _isIntercepting = false;
|
|
bool get isIntercepting => _isIntercepting;
|
|
|
|
/// Initialize the service and start listening for volume button events
|
|
Future<void> init() async {
|
|
if (_isListening) return;
|
|
|
|
_channel.setMethodCallHandler((call) async {
|
|
switch (call.method) {
|
|
case 'volumeUp':
|
|
_volumeUpController.add(null);
|
|
break;
|
|
case 'volumeDown':
|
|
_volumeDownController.add(null);
|
|
break;
|
|
}
|
|
});
|
|
|
|
try {
|
|
await _channel.invokeMethod('startListening');
|
|
_isListening = true;
|
|
_isIntercepting = true;
|
|
} catch (e) {
|
|
_logger.error('Failed to start volume button listening', context: 'VolumeService', error: e);
|
|
}
|
|
}
|
|
|
|
/// Enable or disable volume button interception.
|
|
/// When disabled, hardware volume buttons control device volume normally.
|
|
/// When enabled, volume button events are sent to Flutter for MA player control.
|
|
Future<void> setIntercepting(bool intercept) async {
|
|
if (!_isListening || _isIntercepting == intercept) return;
|
|
|
|
try {
|
|
if (intercept) {
|
|
await _channel.invokeMethod('startListening');
|
|
_isIntercepting = true;
|
|
} else {
|
|
await _channel.invokeMethod('stopListening');
|
|
_isIntercepting = false;
|
|
}
|
|
} catch (e) {
|
|
_logger.error('Failed to set volume interception', context: 'VolumeService', error: e);
|
|
}
|
|
}
|
|
|
|
/// Stop listening for volume button events
|
|
Future<void> dispose() async {
|
|
if (!_isListening) return;
|
|
|
|
try {
|
|
await _channel.invokeMethod('stopListening');
|
|
_isListening = false;
|
|
_isIntercepting = false;
|
|
_logger.info('Hardware volume button listening stopped', context: 'VolumeService');
|
|
} catch (e) {
|
|
_logger.error('Failed to stop volume button listening', context: 'VolumeService', error: e);
|
|
}
|
|
|
|
// Close StreamControllers to prevent memory leaks
|
|
await _volumeUpController.close();
|
|
await _volumeDownController.close();
|
|
}
|
|
}
|