Commit afeb18a
Changed files (8)
app
lib
data_util
features
export_import
input
settings
model
export_import
test
features
input
app/lib/data_util/entry_context.dart
@@ -9,15 +9,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart' hide ProviderNotFoundException;
import 'package:blood_pressure_app/l10n/app_localizations.dart';
import 'package:health_data_store/health_data_store.dart';
+import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
/// Allow high level operations on the repositories in context.
extension EntryUtils on BuildContext {
+ Logger get _logger => Logger('BPM[context.EntryUtils]');
+
/// Open the [AddEntryDialogue] and save received entries.
///
/// Follows [ExportSettings.exportAfterEveryEntry]. When [initial] is not null
/// the dialoge will be opened in edit mode.
Future<void> createEntry([AddEntryFormValue? initial]) async {
+ _logger.finer('createEntry($initial)');
try {
final recordRepo = RepositoryProvider.of<BloodPressureRepository>(this);
final noteRepo = RepositoryProvider.of<NoteRepository>(this);
@@ -29,6 +33,7 @@ extension EntryUtils on BuildContext {
RepositoryProvider.of<MedicineRepository>(this),
initial,
);
+ _logger.finest('received $entry from dialog');
if (entry != null) {
if (initial?.record != null) await recordRepo.remove(initial!.record!);
if (initial?.note != null) await noteRepo.remove(initial!.note!);
@@ -40,8 +45,19 @@ extension EntryUtils on BuildContext {
if (entry.intake != null) await intakeRepo.add(entry.intake!);
if(entry.weight != null) await weightRepo.add(entry.weight!);
+ /*
+ read<IntervalStoreManager>().mainPage.setToMostRecentInterval();
+ read<IntervalStoreManager>().statsPage.setToMostRecentInterval();
+ read<IntervalStoreManager>().exportPage.setToMostRecentInterval();*/
+
+ _logger.finest('mounted=$mounted');
+ if (!mounted) {
+ _logger.warning('Context no longer mounted');
+ return;
+ }
+
+ log.info(read<IntervalStoreManager>());
if (mounted && exportSettings.exportAfterEveryEntry) {
- read<IntervalStoreManager>().exportPage.setToMostRecentInterval();
performExport(this);
}
}
app/lib/features/input/forms/add_entry_form.dart
@@ -41,7 +41,8 @@ class AddEntryForm extends FormBase<AddEntryFormValue> with TypeLogger {
}
/// State of primary form to enter all types of entries.
-class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
+class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm>
+ with TypeLogger {
final _timeForm = GlobalKey<DateTimeFormState>();
final _noteForm = GlobalKey<NoteFormState>();
final _bpForm = GlobalKey<BloodPressureFormState>();
@@ -59,6 +60,7 @@ class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
@override
void initState() {
super.initState();
+ logger.finer('Initializing with ${widget.initialValue}');
if (widget.initialValue != null) {
_lastSavedPressure = widget.initialValue?.record;
_lastSavedWeight = widget.initialValue?.weight;
@@ -95,16 +97,30 @@ class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
}
@override
- bool validate() => !context.read<Settings>().validateInputs
- || (_timeForm.currentState?.validate() ?? false)
- && (_noteForm.currentState?.validate() ?? false)
+ bool validate() {
+ final timeFormValidation = _timeForm.currentState?.validate();
+ final noteFormValidation = _noteForm.currentState?.validate();
+ final bpFormValidation = _bpForm.currentState?.validate();
+ final weightFormValidation = _weightForm.currentState?.validate();
+ final intakeFormValidation = _intakeForm.currentState?.validate();
+ logger.fine('validating...');
+ logger.finest('time: $timeFormValidation');
+ logger.finest('note: $noteFormValidation');
+ logger.finest('bp: $bpFormValidation');
+ logger.finest('weight: $weightFormValidation');
+ logger.finest('intake: $intakeFormValidation');
+ return !context.read<Settings>().validateInputs
+ || (timeFormValidation ?? false)
+ && (noteFormValidation ?? false)
// the following become null when unopened
- && (_bpForm.currentState?.validate() ?? true)
- && (_weightForm.currentState?.validate() ?? true)
- && (_intakeForm.currentState?.validate() ?? true);
+ && (bpFormValidation ?? true)
+ && (weightFormValidation ?? true)
+ && (intakeFormValidation ?? true);
+ }
@override
AddEntryFormValue? save() {
+ logger.fine('Calling save');
if (!validate()) return null;
final time = _timeForm.currentState!.save()!;
Note? note;
@@ -138,11 +154,13 @@ class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
dosis: intakeFormValue.$2,
);
}
+ logger.finer('Saving values: $note, $record, $weight, $intake');
if (note == null
&& record == null
&& weight == null
&& intake == null) {
+ logger.fine('note, record, weight, and intake are null: returning null');
return null;
}
return (
@@ -159,6 +177,7 @@ class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
@override
void fillForm(AddEntryFormValue? value) {
+ logger.finer('fillForm($value)');
_lastSavedPressure = value?.record;
_lastSavedWeight = value?.weight;
_lastSavedIntake = value?.intake;
@@ -260,7 +279,8 @@ class AddEntryFormState extends FormStateBase<AddEntryFormValue, AddEntryForm> {
NoteForm(
key: _noteForm,
initialValue: (){
- if (widget.initialValue?.note?.note == null) return null;
+ logger.fine('NoteForm.initialValue: ${widget.initialValue?.note}');
+ if (widget.initialValue?.note == null) return null;
final note = widget.initialValue!.note!;
final color = note.color == null ? null : Color(note.color!);
return (note.note, color);
@@ -287,7 +307,7 @@ extension AddEntryFormValueCompat on FullEntry {
assert(intakes.length <= 1);
return (
timestamp: time,
- note: (note != null && color == null) ? null : noteObj,
+ note: (note == null && color == null) ? null : noteObj,
record: (sys == null && dia == null && pul == null) ? null : recordObj,
intake: intakes.firstOrNull,
weight: null,
app/lib/features/input/add_entry_dialogue.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:blood_pressure_app/components/fullscreen_dialoge.dart';
import 'package:blood_pressure_app/features/input/forms/add_entry_form.dart';
+import 'package:blood_pressure_app/logging.dart';
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -33,12 +34,13 @@ class AddEntryDialogue extends StatefulWidget {
State<AddEntryDialogue> createState() => _AddEntryDialogueState();
}
-class _AddEntryDialogueState extends State<AddEntryDialogue> {
+class _AddEntryDialogueState extends State<AddEntryDialogue> with TypeLogger {
final formKey = GlobalKey<AddEntryFormState>();
void _onSavePressed() {
if (formKey.currentState?.validate() ?? false) {
final AddEntryFormValue? result = formKey.currentState?.save();
+ logger.finer('Returning result: $result');
Navigator.pop(context, result);
} else {
// Errors are displayed below their specific widgets
app/lib/features/settings/version_screen.dart
@@ -1,13 +1,23 @@
import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
+import 'package:blood_pressure_app/features/settings/tiles/titled_column.dart';
+import 'package:blood_pressure_app/logging.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:blood_pressure_app/l10n/app_localizations.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
-class VersionScreen extends StatelessWidget {
-
+/// Screen that shows app version and debug options.
+class VersionScreen extends StatefulWidget {
+ /// Screen that shows app version and debug options.
const VersionScreen({super.key});
+ @override
+ State<VersionScreen> createState() => _VersionScreenState();
+}
+
+class _VersionScreenState extends State<VersionScreen> with TypeLogger {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
@@ -18,7 +28,7 @@ class VersionScreen extends StatelessWidget {
IconButton(
onPressed: () async {
final packageInfo = await PackageInfo.fromPlatform();
- Clipboard.setData(ClipboardData(
+ await Clipboard.setData(ClipboardData(
text: 'Blood pressure monitor\n'
'${packageInfo.packageName}\n'
'${packageInfo.version} - ${packageInfo.buildNumber}',
@@ -30,20 +40,98 @@ class VersionScreen extends StatelessWidget {
],
backgroundColor: Theme.of(context).primaryColor,
),
- body: Container(
+ body: Padding(
padding: const EdgeInsets.all(10.0),
- child: Center(
- child: ConsistentFutureBuilder<PackageInfo>(
- future: PackageInfo.fromPlatform(),
- onData: (context, packageInfo) => Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(localizations.packageNameOf(packageInfo.packageName)),
- Text(localizations.versionOf(packageInfo.version)),
- Text(localizations.buildNumberOf(packageInfo.buildNumber)),
- ],
+ child: ListView(
+ children: [
+ // Debug info
+ ConsistentFutureBuilder<PackageInfo>(
+ future: PackageInfo.fromPlatform(),
+ onData: (context, packageInfo) => Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(localizations.packageNameOf(packageInfo.packageName)),
+ Text(localizations.versionOf(packageInfo.version)),
+ Text(localizations.buildNumberOf(packageInfo.buildNumber)),
+ ],
+ ),
+ ),
+ // Logs
+ SwitchListTile(
+ // Would not be used by regular users so no need to translate
+ title: Text('Enable ultra-verbose logging until app restart'),
+ subtitle: Text('This can help to track down hard to reproduce bugs'),
+ value: Log.isVerbose,
+ onChanged: (v) => setState(() => Log.setVerbose(v)),
+ ),
+ ListTile(
+ title: Text('Logs:'),
+ trailing: Icon(Icons.copy),
+ onTap: () async {
+ await Clipboard.setData(ClipboardData(
+ text: Log.logs
+ .map((e) => '${e.level.name} - ${e.time.toIso8601String()}||'
+ '${e.loggerName}||"${e.message}"||{${e.stackTrace}}\n')
+ .fold('', (res, e) => res + e),
+ ));
+ if(context.mounted){
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text('Logs copied to clipboard'),
+ ));
+ }
+ },
+ ),
+ SizedBox(
+ height: 600,
+ child: ListView.builder(
+ shrinkWrap: true,
+ itemCount: Log.logs.length,
+ itemBuilder: (context, idx) {
+ final record = Log.logs[Log.logs.length - idx - 1];
+ return ExpansionTile(
+ title: Wrap(
+ spacing: 4.0,
+ crossAxisAlignment: WrapCrossAlignment.center,
+ //mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Chip(
+ // FIXME: doesn't work in light mode
+ backgroundColor: switch(record.level.value) {
+ <= 500 => Colors.transparent,
+ <= 800 => Colors.grey.shade800,
+ <= 900 => Colors.deepOrange,
+ <= 1000 => Colors.red,
+ int() => Colors.red.shade900,
+ },
+ label: Text(record.level.name),
+ ),
+ Text(record.loggerName),
+ ],
+ ),
+ subtitle: Text('Timestamp: ${record.time.hour}:${record.time.minute}.${record.time.second}'),
+ children: [
+ Text(record.message),
+ if (record.stackTrace != null)
+ Text(record.stackTrace.toString()),
+ ],
+ );
+ },
),
- ),
+ ),
+ ListTile(
+ title: Text('Test log messages'),
+ trailing: Icon(Icons.chevron_right),
+ onTap: () {
+ logger.finest('test finest');
+ logger.finer('test finer');
+ logger.fine('test fine');
+ logger.info('test info');
+ logger.warning('test warning');
+ logger.severe('test severe');
+ logger.shout('test shout');
+ },
+ )
+ ],
),
),
);
app/lib/model/export_import/csv_converter.dart
@@ -1,3 +1,4 @@
+import 'package:blood_pressure_app/logging.dart';
import 'package:blood_pressure_app/model/export_import/column.dart';
import 'package:blood_pressure_app/model/export_import/import_field_type.dart' show RowDataFieldType;
import 'package:blood_pressure_app/model/export_import/record_parsing_result.dart';
@@ -8,9 +9,15 @@ import 'package:csv/csv.dart';
import 'package:health_data_store/health_data_store.dart';
/// Utility class to convert between csv strings and [BloodPressureRecord]s.
-class CsvConverter {
+class CsvConverter with TypeLogger {
/// Create converter between csv strings and [BloodPressureRecord] values that respects settings.
- CsvConverter(this.settings, this.availableColumns, this.availableMedicines);
+ CsvConverter(this.settings, this.availableColumns, this.availableMedicines) {
+ logger.fine('Creating CsvConverter with '
+ 'settings=${settings.toJson()}, '
+ 'availableColumns=$availableColumns, ',
+ 'availableMedicines=$availableMedicines'
+ );
+ }
/// Settings that apply for ex- and import.
final CsvExportSettings settings;
app/lib/logging.dart
@@ -18,9 +18,18 @@ mixin TypeLogger {
///
/// Also contains some logging configuration logic
class Log {
- /// Whether logging is enabled
+ static final _verboseLevel = Level.ALL;
+ static final _normalLevel = Level.WARNING;
+
+ /// Logs recorded this session.
+ static final logs = <LogRecord>[];
+
+ /// Whether debug logging is enabled.
static final enabled = kDebugMode && !isTestingEnvironment;
+ /// Whether verbose logging is activated.
+ static bool get isVerbose => Logger.root.level == Level.ALL;
+
/// Format a log record
static String format(LogRecord record) {
final loggerName = record.loggerName == 'BloodPressureMonitor' ? null : record.loggerName;
@@ -32,9 +41,20 @@ class Log {
/// Register the apps logging config with [Logger].
static void setup() {
+ Logger.root.onRecord.listen(logs.add);
if (Log.enabled) {
- Logger.root.level = Level.ALL;
+ Logger.root.level = _verboseLevel;
Logger.root.onRecord.listen((record) => debugPrint(Log.format(record)));
+ } else {
+ Logger.root.level = _normalLevel;
}
}
+
+ /// Set ultra verbose(true) or normal logging(false).
+ static void setVerbose(bool isVerbose) {
+ Logger.root.level = isVerbose
+ ? _verboseLevel
+ : _normalLevel;
+ Logger.root.info('Verbose logging set to $isVerbose');
+ }
}
app/test/features/input/forms/add_entry_form_test.dart
@@ -17,6 +17,7 @@ import 'package:health_data_store/health_data_store.dart';
import 'package:intl/intl.dart';
import '../../../model/analyzer_test.dart';
+import '../../../model/export_import/record_formatter_test.dart';
import '../../../util.dart';
import '../../measurement_list/measurement_list_entry_test.dart';
@@ -436,6 +437,11 @@ void main() {
expect(find.byType(BloodPressureForm), findsNothing);
expect(find.byType(WeightForm), findsOneWidget);
});
+
+ test('correctly creates AddEntryFormValue from note only FullEntry', () {
+ final FullEntry entry = mockEntry(note: 'Test');
+ expect(entry.asAddEntry.note?.note, 'Test');
+ });
}
class _MockBluetoothCubit extends Fake implements BluetoothCubit {