import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/globals.dart'; void initLogger() { // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) async { await _writeLogToFile(record); if (!kReleaseMode) { // ignore: avoid_print print( '${record.level.name} [twonly] ${record.loggerName} > ${record.message}', ); } }); cleanLogFile(); } class Log { static String filterLogMessage(String msg) { if (msg.contains('SqliteException')) { // Do not log data which would be inserted into the DB. return msg.substring(0, msg.indexOf('parameters: ')); } return msg; } static void error( Object? messageInput, [ Object? error, StackTrace? stackTrace, ]) { final message = filterLogMessage('$messageInput'); if (globalAllowErrorTrackingViaSentry) { try { throw Exception(message); } catch (exception, stackTrace) { Sentry.captureException(exception, stackTrace: stackTrace); } } Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace); } static void warn( Object? messageInput, [ Object? error, StackTrace? stackTrace, ]) { final message = filterLogMessage('$messageInput'); Logger(_getCallerSourceCodeFilename()).warning(message, error, stackTrace); } static void info( Object? messageInput, [ Object? error, StackTrace? stackTrace, ]) { final message = filterLogMessage('$messageInput'); Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace); } } Mutex writeToLogGuard = Mutex(); Future loadLogFile() async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); if (logFile.existsSync()) { return logFile.readAsString(); } else { return 'Log file does not exist.'; } } Future readLast1000Lines() async { final dir = await getApplicationSupportDirectory(); final file = File('${dir.path}/app.log'); if (!file.existsSync()) return ''; final all = await file.readAsLines(); final start = all.length > 1000 ? all.length - 1000 : 0; return all.sublist(start).join('\n'); } Future _writeLogToFile(LogRecord record) async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); // Prepare the log message final logMessage = '${DateTime.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n'; await writeToLogGuard.protect(() async { // Append the log message to the file await logFile.writeAsString(logMessage, mode: FileMode.append); }); } Future cleanLogFile() async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); if (logFile.existsSync()) { final lines = await logFile.readAsLines(); if (lines.length <= 5000) return; final removeCount = lines.length - 5000; final remaining = lines.sublist(removeCount); final sink = logFile.openWrite()..writeAll(remaining, '\n'); await sink.close(); } } Future deleteLogFile() async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); if (logFile.existsSync()) { await logFile.delete(); return true; } return false; } String _getCallerSourceCodeFilename() { final stackTrace = StackTrace.current; final stackTraceString = stackTrace.toString(); var fileName = ''; var lineNumber = ''; final stackLines = stackTraceString.split('\n'); if (stackLines.length > 2) { final callerInfo = stackLines[2]; final parts = callerInfo.split('/'); fileName = parts.last.split(':').first; // Extract the file name lineNumber = parts.last.split(':')[1]; // Extract the line number } else { final firstLine = stackTraceString.split('\n')[0]; fileName = firstLine.split('/').last.split(':').first; // Extract the file name lineNumber = firstLine.split(':')[1]; // Extract the line number } return '$fileName:$lineNumber'; }