refactor(logging): isolate native log transport

This commit is contained in:
zarzet committed 2026-10-06 13:07:57 +07:00
1 parent a6c1409ef5
commit 08e4f1b41d
4 files changed
+97 -23

No files matched your search

+30
View File
@@ -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<Map<String, dynamic>> getSince(int index) async {
final result = await _channel.invokeMethod<Object?>('getLogsSince', {
'index': index,
});
final decoded = result is String
? (result.isEmpty ? null : jsonDecode(result))
: result;
if (decoded is Map) return decoded.cast<String, dynamic>();
throw FormatException(
'Expected map result from getLogsSince, got ${decoded.runtimeType}',
);
}
Future<void> setLoggingEnabled(bool enabled) =>
_channel.invokeMethod<void>('setLoggingEnabled', {'enabled': enabled});
Future<void> clear() => _channel.invokeMethod<void>('clearLogs');
}
-15
View File
@@ -1726,17 +1726,6 @@ class PlatformBridge {
);
}
static Future<Map<String, dynamic>> getGoLogsSince(int index) async {
final result = await _channel.invokeMethod('getLogsSince', {
'index': index,
});
return _decodeRequiredMapResult(result, 'getGoLogsSince');
}
static Future<void> 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<void> releaseNativeMemory({bool underPressure = false}) async {
@@ -1755,10 +1744,6 @@ class PlatformBridge {
} catch (_) {}
}
static Future<void> setGoLoggingEnabled(bool enabled) async {
await _channel.invokeMethod('setLoggingEnabled', {'enabled': enabled});
}
static Future<void> initExtensionSystem(
String extensionsDir,
String dataDir, {
+5 -8
View File
@@ -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<LogEntry> _entries = Queue<LogEntry>();
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<LogEntry> get entries => _entries.toList();
@@ -209,7 +206,7 @@ class LogBuffer extends ChangeNotifier {
Future<void> _fetchGoLogs() async {
try {
final result = await PlatformBridge.getGoLogsSince(_lastGoLogIndex);
final result = await _nativeLogs.getSince(_lastGoLogIndex);
final logs = result['logs'] as List<dynamic>? ?? [];
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();
}
+62
View File
@@ -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 = <MethodCall>[];
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<PlatformException>()));
});
}