From 08e4f1b41de088231db3e38d2652bfd176e1aa87 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:57 +0700 Subject: [PATCH] refactor(logging): isolate native log transport --- lib/services/native_log_source.dart | 30 ++++++++++++++ lib/services/platform_bridge.dart | 15 ------- lib/utils/logger.dart | 13 +++--- test/native_log_source_test.dart | 62 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 lib/services/native_log_source.dart create mode 100644 test/native_log_source_test.dart diff --git a/lib/services/native_log_source.dart b/lib/services/native_log_source.dart new file mode 100644 index 00000000..f2ce6627 --- /dev/null +++ b/lib/services/native_log_source.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +/// Logging transport is independent of the bridge whose calls it records. +class NativeLogSource { + const NativeLogSource({ + MethodChannel channel = const MethodChannel('com.zarz.spotiflac/backend'), + }) : _channel = channel; + + final MethodChannel _channel; + + Future> getSince(int index) async { + final result = await _channel.invokeMethod('getLogsSince', { + 'index': index, + }); + final decoded = result is String + ? (result.isEmpty ? null : jsonDecode(result)) + : result; + if (decoded is Map) return decoded.cast(); + throw FormatException( + 'Expected map result from getLogsSince, got ${decoded.runtimeType}', + ); + } + + Future setLoggingEnabled(bool enabled) => + _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled}); + + Future clear() => _channel.invokeMethod('clearLogs'); +} diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 29cdcf87..cc6701b7 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1726,17 +1726,6 @@ class PlatformBridge { ); } - static Future> getGoLogsSince(int index) async { - final result = await _channel.invokeMethod('getLogsSince', { - 'index': index, - }); - return _decodeRequiredMapResult(result, 'getGoLogsSince'); - } - - static Future clearGoLogs() async { - await _channel.invokeMethod('clearLogs'); - } - /// Ask the native backend to release unused memory. Best-effort: /// safe to call on memory pressure or when the app is backgrounded. static Future releaseNativeMemory({bool underPressure = false}) async { @@ -1755,10 +1744,6 @@ class PlatformBridge { } catch (_) {} } - static Future setGoLoggingEnabled(bool enabled) async { - await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled}); - } - static Future initExtensionSystem( String extensionsDir, String dataDir, { diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 58ea3233..283cf7cf 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:spotiflac_android/constants/app_info.dart'; -import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/services/native_log_source.dart'; const int _maxLogMessageLength = 500; const int _maxBufferedLogMessageLength = 4000; @@ -121,6 +121,7 @@ class LogBuffer extends ChangeNotifier { LogBuffer._internal(); static const int maxEntries = 500; + static const _nativeLogs = NativeLogSource(); static const Duration _goLogPollingInterval = Duration(milliseconds: 800); final Queue _entries = Queue(); Timer? _goLogTimer; @@ -131,11 +132,7 @@ class LogBuffer extends ChangeNotifier { static bool get loggingEnabled => _loggingEnabled; static set loggingEnabled(bool value) { _loggingEnabled = value; - if (value) { - PlatformBridge.setGoLoggingEnabled(true).catchError((_) {}); - } else { - PlatformBridge.setGoLoggingEnabled(false).catchError((_) {}); - } + _nativeLogs.setLoggingEnabled(value).catchError((_) {}); } List get entries => _entries.toList(); @@ -209,7 +206,7 @@ class LogBuffer extends ChangeNotifier { Future _fetchGoLogs() async { try { - final result = await PlatformBridge.getGoLogsSince(_lastGoLogIndex); + final result = await _nativeLogs.getSince(_lastGoLogIndex); final logs = result['logs'] as List? ?? []; final nextIndex = result['next_index'] as int? ?? _lastGoLogIndex; final keepNonErrorLogs = _loggingEnabled; @@ -268,7 +265,7 @@ class LogBuffer extends ChangeNotifier { void clear() { _entries.clear(); _lastGoLogIndex = 0; - PlatformBridge.clearGoLogs().catchError((_) {}); + _nativeLogs.clear().catchError((_) {}); notifyListeners(); } diff --git a/test/native_log_source_test.dart b/test/native_log_source_test.dart new file mode 100644 index 00000000..8a8743e8 --- /dev/null +++ b/test/native_log_source_test.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/services/native_log_source.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const channel = MethodChannel('com.zarz.spotiflac/backend'); + const source = NativeLogSource(channel: channel); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + tearDown(() => messenger.setMockMethodCallHandler(channel, null)); + + for (final encoded in [false, true]) { + test('reads native logs and cursor (JSON=$encoded)', () async { + final payload = { + 'logs': [ + {'level': 'ERROR', 'tag': 'Native', 'message': 'failed'}, + ], + 'next_index': 8, + }; + messenger.setMockMethodCallHandler(channel, (call) async { + expect(call.method, 'getLogsSince'); + expect(call.arguments, {'index': 3}); + return encoded ? jsonEncode(payload) : payload; + }); + expect(await source.getSince(3), payload); + }); + } + + test('keeps the native logging controls on the existing channel', () async { + final calls = []; + messenger.setMockMethodCallHandler(channel, (call) async { + calls.add(call); + return null; + }); + await source.setLoggingEnabled(true); + await source.setLoggingEnabled(false); + await source.clear(); + expect(calls.map((call) => call.method), [ + 'setLoggingEnabled', + 'setLoggingEnabled', + 'clearLogs', + ]); + expect(calls[0].arguments, {'enabled': true}); + expect(calls[1].arguments, {'enabled': false}); + }); + + test('reports malformed batches instead of silently losing logs', () async { + messenger.setMockMethodCallHandler(channel, (_) async => '[]'); + await expectLater(source.getSince(0), throwsFormatException); + }); + + test('propagates transport failures to the polling error boundary', () async { + messenger.setMockMethodCallHandler(channel, (_) async { + throw PlatformException(code: 'backend_unavailable'); + }); + await expectLater(source.getSince(0), throwsA(isA())); + }); +}