Commit a33cbb71d09d06c04957c021e8bb03dc82632bc0

Authored by 权海
1 parent cd2e364f

feat(ui):优化ohos日志

@@ -7,7 +7,7 @@ import 'package:path_provider/path_provider.dart'; @@ -7,7 +7,7 @@ import 'package:path_provider/path_provider.dart';
7 7
8 import 'app_logger.dart'; 8 import 'app_logger.dart';
9 9
10 -/// Persists a compact, privacy-safe timeline of the OHOS health data pipeline. 10 +/// Persists a privacy-safe timeline of the OHOS health data pipeline.
11 /// 11 ///
12 /// Writes are queued and intentionally fire-and-forget so health callbacks, 12 /// Writes are queued and intentionally fire-and-forget so health callbacks,
13 /// calculation, and push delivery are never blocked by file I/O. Call 13 /// calculation, and push delivery are never blocked by file I/O. Call
@@ -18,9 +18,10 @@ class OhosHealthFlowLogStore { @@ -18,9 +18,10 @@ class OhosHealthFlowLogStore {
18 18
19 static final OhosHealthFlowLogStore instance = OhosHealthFlowLogStore(); 19 static final OhosHealthFlowLogStore instance = OhosHealthFlowLogStore();
20 20
21 - static const fileName = 'ohos_health_flow_events.json';  
22 - static const _retention = Duration(days: 7);  
23 - static const _maxEventCount = 1000; 21 + static const _fileNamePrefix = 'ohos_health_flow_events_';
  22 + static const _fileNameSuffix = '.json';
  23 + static const _legacyFileName = 'ohos_health_flow_events.json';
  24 + static const _retainedDays = 7;
24 25
25 final Directory? _rootDirectory; 26 final Directory? _rootDirectory;
26 Future<void> _writeQueue = Future<void>.value(); 27 Future<void> _writeQueue = Future<void>.value();
@@ -49,18 +50,15 @@ class OhosHealthFlowLogStore { @@ -49,18 +50,15 @@ class OhosHealthFlowLogStore {
49 if (details != null && details.isNotEmpty) 50 if (details != null && details.isNotEmpty)
50 'details': _sanitizeMap(details), 51 'details': _sanitizeMap(details),
51 }; 52 };
52 - final file = await _file();  
53 - await file.parent.create(recursive: true);  
54 - final cutoff = now.subtract(_retention);  
55 - final events = (await _readEvents(file))  
56 - .where((item) => _isWithinRetention(item, cutoff))  
57 - .toList()  
58 - ..add(event);  
59 - final retained = events.length > _maxEventCount  
60 - ? events.sublist(events.length - _maxEventCount)  
61 - : events; 53 + final root = await _root();
  54 + await root.create(recursive: true);
  55 + await _migrateLegacyFile(root, now);
  56 + await _deleteExpiredDailyFiles(root, now);
  57 + final file = _fileFor(root, now);
  58 + final events = await _readEvents(file);
  59 + events.add(event);
62 await file.writeAsString( 60 await file.writeAsString(
63 - const JsonEncoder.withIndent(' ').convert(retained), 61 + const JsonEncoder.withIndent(' ').convert(events),
64 flush: true, 62 flush: true,
65 ); 63 );
66 } catch (error, stackTrace) { 64 } catch (error, stackTrace) {
@@ -77,7 +75,7 @@ class OhosHealthFlowLogStore { @@ -77,7 +75,7 @@ class OhosHealthFlowLogStore {
77 /// Returns the complete file after all previously queued writes finish. 75 /// Returns the complete file after all previously queued writes finish.
78 Future<Uint8List> readBytes() async { 76 Future<Uint8List> readBytes() async {
79 await _writeQueue; 77 await _writeQueue;
80 - final file = await _file(); 78 + final file = _fileFor(await _root(), DateTime.now());
81 if (!await file.exists()) { 79 if (!await file.exists()) {
82 return Uint8List.fromList( 80 return Uint8List.fromList(
83 utf8.encode('[{"message":"No OHOS health flow logs found"}]'), 81 utf8.encode('[{"message":"No OHOS health flow logs found"}]'),
@@ -86,11 +84,79 @@ class OhosHealthFlowLogStore { @@ -86,11 +84,79 @@ class OhosHealthFlowLogStore {
86 return file.readAsBytes(); 84 return file.readAsBytes();
87 } 85 }
88 86
89 - Future<File> _file() async {  
90 - final root = _rootDirectory ?? await getApplicationDocumentsDirectory();  
91 - return File('${root.path}/$fileName'); 87 + Future<Directory> _root() {
  88 + return _rootDirectory == null
  89 + ? getApplicationDocumentsDirectory()
  90 + : Future<Directory>.value(_rootDirectory);
92 } 91 }
93 92
  93 + File _fileFor(Directory root, DateTime date) {
  94 + return File('${root.path}/${_fileNameFor(date)}');
  95 + }
  96 +
  97 + static String _fileNameFor(DateTime date) {
  98 + final day = '${date.year.toString().padLeft(4, '0')}'
  99 + '${date.month.toString().padLeft(2, '0')}'
  100 + '${date.day.toString().padLeft(2, '0')}';
  101 + return '$_fileNamePrefix$day$_fileNameSuffix';
  102 + }
  103 +
  104 + Future<void> _deleteExpiredDailyFiles(Directory root, DateTime now) async {
  105 + final earliestRetainedDay = DateTime(now.year, now.month, now.day)
  106 + .subtract(const Duration(days: _retainedDays - 1));
  107 + await for (final entity in root.list(followLinks: false)) {
  108 + if (entity is! File) continue;
  109 + final date = _dateFromFileName(entity.uri.pathSegments.last);
  110 + if (date != null && date.isBefore(earliestRetainedDay)) {
  111 + await entity.delete();
  112 + }
  113 + }
  114 + }
  115 +
  116 + Future<void> _migrateLegacyFile(Directory root, DateTime now) async {
  117 + final legacyFile = File('${root.path}/$_legacyFileName');
  118 + if (!await legacyFile.exists()) return;
  119 +
  120 + final earliestRetainedDay = DateTime(now.year, now.month, now.day)
  121 + .subtract(const Duration(days: _retainedDays - 1));
  122 + final eventsByDay = <String, List<Map<String, Object?>>>{};
  123 + for (final event in await _readEvents(legacyFile)) {
  124 + final timestamp = event['timestamp'];
  125 + final date = timestamp is String ? DateTime.tryParse(timestamp) : null;
  126 + if (date == null || _dayOf(date).isBefore(earliestRetainedDay)) continue;
  127 + eventsByDay.putIfAbsent(_fileNameFor(date), () => []).add(event);
  128 + }
  129 +
  130 + try {
  131 + for (final entry in eventsByDay.entries) {
  132 + final file = File('${root.path}/${entry.key}');
  133 + final events = await _readEvents(file);
  134 + events.addAll(entry.value);
  135 + await file.writeAsString(
  136 + const JsonEncoder.withIndent(' ').convert(events),
  137 + flush: true,
  138 + );
  139 + }
  140 + await legacyFile.delete();
  141 + } catch (_) {
  142 + // Keep the legacy file for the next write if migration cannot finish.
  143 + }
  144 + }
  145 +
  146 + static DateTime? _dateFromFileName(String fileName) {
  147 + final match =
  148 + RegExp(r'^ohos_health_flow_events_(\d{8})\.json$').firstMatch(fileName);
  149 + if (match == null) return null;
  150 + final day = match.group(1)!;
  151 + final date = DateTime.tryParse(
  152 + '${day.substring(0, 4)}-${day.substring(4, 6)}-${day.substring(6, 8)}',
  153 + );
  154 + return date != null && _fileNameFor(date) == fileName ? date : null;
  155 + }
  156 +
  157 + static DateTime _dayOf(DateTime date) =>
  158 + DateTime(date.year, date.month, date.day);
  159 +
94 Future<List<Map<String, Object?>>> _readEvents(File file) async { 160 Future<List<Map<String, Object?>>> _readEvents(File file) async {
95 if (!await file.exists()) return <Map<String, Object?>>[]; 161 if (!await file.exists()) return <Map<String, Object?>>[];
96 try { 162 try {
@@ -127,14 +193,4 @@ class OhosHealthFlowLogStore { @@ -127,14 +193,4 @@ class OhosHealthFlowLogStore {
127 193
128 static String _limit(String value) => 194 static String _limit(String value) =>
129 value.length <= 2000 ? value : '${value.substring(0, 2000)}…'; 195 value.length <= 2000 ? value : '${value.substring(0, 2000)}…';
130 -  
131 - static bool _isWithinRetention(  
132 - Map<String, Object?> event,  
133 - DateTime cutoff,  
134 - ) {  
135 - final timestamp = event['timestamp'];  
136 - if (timestamp is! String) return false;  
137 - final recordedAt = DateTime.tryParse(timestamp);  
138 - return recordedAt != null && !recordedAt.isBefore(cutoff);  
139 - }  
140 } 196 }