cph-cachet / flutter-plugins

A collection of Flutter plugins developed by CACHET
547 stars 664 forks source link

[Health 8.0.0] Retrieving even a single iOS ECG measurement w/ voltage data causes app to crash #819

Open maxbeech opened 1 year ago

maxbeech commented 1 year ago

Hi there, thanks so much for the fantastic package!!

I am trying to retrieve iOS ECG data including voltage information within my Flutter app. Sadly the app crashes when I try to retrieve just a single ECG reading. I have tracked them back and I'm pretty sure they crash within the Swift file at the HKElectrocardiogramQuery(ecg_sample_here) function ie. when it's actually retrieving the data (relevant Apple documentation here).

Whenever I do it, I get the memory error:

* thread #3, queue = 'com.apple.HealthKit.HKHealthStore.client.0x281d32630', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 0: (Runner) stopped.

I get the same error on both an iPhone 7 and iPhone 11 (both running min. iOS 14) using various different readings.

Does anyone know how to avoid this happening please?

I'm not sure if say there's a way to retrieve a part of a reading for example, as I think the app is just struggling to handle the size of variable.

Thanks a lot!

hoffmatteo commented 1 year ago

Hi, I just tested by reading an ECG measurement from an Apple watch, which worked normally for me: flutter: HealthDataPoint - value: 15360 values, 60.0 BPM, 512.0 HZ, ElectrocardiogramClassification.SINUS_RHYTHM, unit: VOLT, dateFrom: 2023-10-11 12:51:16.071, dateTo: 2023-10-11 12:51:46.071, dataType: ELECTROCARDIOGRAM, platform: PlatformType.IOS, deviceId:, sourceId: com.apple.NanoHeartRhythm, sourceName: EKG

Can you describe your steps in more detail? Are you using the example app or your own app? If you use the example app, does the measurement show up correctly?

maxbeech commented 1 year ago

Many thanks for the reply @hoffmatteo. I was using my own app, however I have just tried it on the example app and the same issue occurred. Do you have any suggestions please? Thanks a ton..

The reading I am trying to retrieve (have tried others unsuccessfully too):

IMG_896B263150C1-1

My device:

iPhone 7, iOS 15.7.3

Here's the full output:

Launching lib/main.dart on iPhone (2) in debug mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: E353LGUVGH
Running Xcode build...
Xcode build done.                                           15.0s
Installing and launching...
(lldb) 2023-10-12 14:37:48.457536+0100 Runner[577:18074] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(37)] Using the Impeller rendering backend.
Warning: Unable to create restoration in progress marker file
fopen failed for data file: errno = 2 (No such file or directory)
Errors found! Invalidating cache...
Debug service listening on ws://127.0.0.1:63188/JLkk8ZIowIU=/ws
Syncing files to device iPhone (2)...
* thread #25, queue = 'com.apple.HealthKit.HKHealthStore.client.0x2812b8750', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
error: memory read failed for 0x0
Target 0: (Runner) stopped.
Lost connection to device.

Here's the full main.dart:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:health/health.dart';

void main() => runApp(HealthApp());

class HealthApp extends StatefulWidget {
  @override
  _HealthAppState createState() => _HealthAppState();
}

enum AppState {
  DATA_NOT_FETCHED,
  FETCHING_DATA,
  DATA_READY,
  NO_DATA,
  AUTHORIZED,
  AUTH_NOT_GRANTED,
  DATA_ADDED,
  DATA_DELETED,
  DATA_NOT_ADDED,
  DATA_NOT_DELETED,
  STEPS_READY,
}

class _HealthAppState extends State<HealthApp> {
  List<HealthDataPoint> _healthDataList = [];
  AppState _state = AppState.DATA_NOT_FETCHED;
  static final types = [
    HealthDataType.ELECTROCARDIOGRAM
  ];
  final permissions = types.map((e) => HealthDataAccess.READ).toList();
  HealthFactory health = HealthFactory(useHealthConnectIfAvailable: true);

  Future authorize() async {
    // Check if we have permission
    bool? hasPermissions =
        await health.hasPermissions(types, permissions: permissions);
    hasPermissions = false;

    bool authorized = false;
    if (!hasPermissions) {
      // requesting access to the data types before reading them
      try {
        authorized =
            await health.requestAuthorization(types, permissions: permissions);
      } catch (error) {
        print("Exception in authorize: $error");
      }
    }
    setState(() => _state =
        (authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED);
  }

  Future fetchData() async {
    setState(() => _state = AppState.FETCHING_DATA);
    final start = DateTime(2022,03,31);
    final end = DateTime(2022,04,02);
    _healthDataList.clear();

    try {
      List<HealthDataPoint> healthData =
          await health.getHealthDataFromTypes(start, end, types);
      _healthDataList.addAll(
          (healthData.length < 100) ? healthData : healthData.sublist(0, 100));
    } catch (error) {
      print("Exception in getHealthDataFromTypes: $error");
    }
    _healthDataList = HealthFactory.removeDuplicates(_healthDataList);
    _healthDataList.forEach((x) => print(x));
    setState(() {
      _state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
    });
  }

  Widget _contentFetchingData() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Container(
            padding: EdgeInsets.all(20),
            child: CircularProgressIndicator(
              strokeWidth: 10,
            )),
        Text('Fetching data...')
      ],
    );
  }

  Widget _contentDataReady() {
    return ListView.builder(
        itemCount: _healthDataList.length,
        itemBuilder: (_, index) {
          HealthDataPoint p = _healthDataList[index];
          if (p.value is AudiogramHealthValue) {
            return ListTile(
              title: Text("${p.typeString}: ${p.value}"),
              trailing: Text('${p.unitString}'),
              subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
            );
          }
          if (p.value is WorkoutHealthValue) {
            return ListTile(
              title: Text(
                  "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"),
              trailing: Text(
                  '${(p.value as WorkoutHealthValue).workoutActivityType.name}'),
              subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
            );
          }
          return ListTile(
            title: Text("${p.typeString}: ${p.value}"),
            trailing: Text('${p.unitString}'),
            subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
          );
        });
  }

  Widget _contentNoData() {
    return Text('No Data to show');
  }

  Widget _contentNotFetched() {
    return Column(
      children: [
        Text('Press the download button to fetch data.'),
        Text('Press the plus button to insert some random data.'),
        Text('Press the walking button to get total step count.'),
      ],
      mainAxisAlignment: MainAxisAlignment.center,
    );
  }

  Widget _authorized() {
    return Text('Authorization granted!');
  }

  Widget _authorizationNotGranted() {
    return Text('Authorization not given. '
        'For Android please check your OAUTH2 client ID is correct in Google Developer Console. '
        'For iOS check your permissions in Apple Health.');
  }

  Widget _content() {
    if (_state == AppState.DATA_READY)
      return _contentDataReady();
    else if (_state == AppState.NO_DATA)
      return _contentNoData();
    else if (_state == AppState.FETCHING_DATA)
      return _contentFetchingData();
    else if (_state == AppState.AUTHORIZED)
      return _authorized();
    else if (_state == AppState.AUTH_NOT_GRANTED)
      return _authorizationNotGranted();
    else
      return _contentNotFetched();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Health Example'),
        ),
        body: Container(
          child: Column(
            children: [
              Wrap(
                spacing: 10,
                children: [
                  TextButton(
                      onPressed: authorize,
                      child:
                          Text("Auth", style: TextStyle(color: Colors.white)),
                      style: ButtonStyle(
                          backgroundColor:
                              MaterialStatePropertyAll(Colors.blue))),
                  TextButton(
                      onPressed: fetchData,
                      child: Text("Fetch Data",
                          style: TextStyle(color: Colors.white)),
                      style: ButtonStyle(
                          backgroundColor:
                              MaterialStatePropertyAll(Colors.blue))),
                ],
              ),
              Divider(thickness: 3),
              Expanded(child: Center(child: _content()))
            ],
          ),
        ),
      ),
    );
  }
}