diff --git a/.github/ISSUE_TEMPLATE/bugfix.md b/.github/ISSUE_TEMPLATE/bugfix.md deleted file mode 100644 index 6edbee250..000000000 --- a/.github/ISSUE_TEMPLATE/bugfix.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Bug fix -about: Create a report to help us improve any bug you may experience. -title: '[Specify the plugin name and version here]' -labels: 'bugfix' -assignees: '' - ---- - -**Remember to specify the plugin name in the title!** - -### Device / Emulator and OS -Please complete the following information for each phone and/or emulator you're experiencing this bug on: - - Device: [e.g. iPhone 6s] - - OS: [e.g. iOS 13.1] - -**NB: Bugs pertaining to old devices/OS versions will likely not be fixed.** -* For iOS only the latest OS is supported. -* For Android, see [the OS versions for which Google support security fixes](https://en.wikipedia.org/wiki/Android_version_history)** - -### Describe the bug -A clear and concise description of what the bug is. - -### To Reproduce -Steps to reproduce the behavior. - -### Expected behavior -What did you expect to happen? - -### Actual behavior -What did happen? Include stack traces and exception print-outs. - -### Screenshots -If applicable, add screenshots to help explain your problem. - -### Flutter doctor -Please run `flutter doctor` and add the output here. - -### Additional information -Add any other info about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bugfix.yml b/.github/ISSUE_TEMPLATE/bugfix.yml new file mode 100644 index 000000000..93704b406 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bugfix.yml @@ -0,0 +1,145 @@ +name: 🐞 Bug fix +description: Create a report to help us improve any bug you may experience. +title: "[ ] a brief, descriptive title for the bug" +labels: ["bugfix"] +assignees: [] +body: + - type: markdown + attributes: + value: | + ## Bug Report Details + Please fill in the information below to help us understand and fix the bug. + + - type: dropdown + id: plugin + attributes: + label: Plugin Name + description: Select the plugin that has the bug + options: + - screen_state + - light + - pedometer + - noise_meter + - app_usage + - weather + - air_quality + - notifications + - movisens_flutter + - esense_flutter + - health + - activity_recognition + - audio_streamer + - mobility_features + - carp_background_location + - flutter_foreground_service + validations: + required: true + + - type: input + id: version + attributes: + label: Plugin Version + description: What version of the plugin are you using? + placeholder: "e.g. 2.4.1" + validations: + required: true + + - type: markdown + attributes: + value: | + ### Device / Emulator and OS Information + Please provide information for each device/emulator where you experience this bug. + + **Note:** + - For iOS, only the latest OS is supported + - For Android, see [the OS versions for which Google support security fixes](https://en.wikipedia.org/wiki/Android_version_history) + - Bugs pertaining to old devices/OS versions will likely not be fixed. + + - type: input + id: device + attributes: + label: Device + description: What device are you using? + placeholder: "e.g. iPhone 6s" + validations: + required: true + + - type: input + id: os + attributes: + label: Operating System + description: What OS version is installed? + placeholder: "e.g. iOS 13.1" + validations: + required: true + + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: What actually happened? Include stack traces and exception print-outs. + validations: + required: true + + - type: textarea + id: flutter-logs + attributes: + label: Flutter Logs + description: Add flutter logs in here. + render: shell + validations: + required: false + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + validations: + required: false + + - type: textarea + id: flutter-doctor + attributes: + label: Flutter Doctor Output + description: Please run `flutter doctor` and paste the output here. + render: shell + validations: + required: true + + - type: textarea + id: additional-info + attributes: + label: Additional Information + description: Add any other relevant information about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..ec4bb386b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index f66dbae2d..8bb6a7a81 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ app.*.symbols !/dev/ci/**/Gemfile.lock packages/app_usage/example/.flutter-plugins-dependencies packages/app_usage/example/.flutter-plugins-dependencies + +.sdkmanrc \ No newline at end of file diff --git a/README.md b/README.md index 548cf3bb3..5326c9bf4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ These are the available plugins in this repository. | Plugin | Description | Android | iOS | http://pub.dev/ | |--------|-------------|:-------:|:---:|:---------:| -| [screen_state](./packages/screen_state) | Track screen state changes | ✔️ | ❌ | [![pub package](https://img.shields.io/pub/v/screen_state.svg)](https://pub.dartlang.org/packages/screen_state) | +| [screen_state](./packages/screen_state) | Track screen state changes | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/screen_state.svg)](https://pub.dartlang.org/packages/screen_state) | | [light](./packages/light) | Track light sensor readings | ✔️ | ❌ | [![pub package](https://img.shields.io/pub/v/light.svg)](https://pub.dartlang.org/packages/light) | | [pedometer](./packages/pedometer) | Track step count | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/pedometer.svg)](https://pub.dartlang.org/packages/pedometer) | | [noise_meter](./packages/noise_meter) | Read noise level in Decibel | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/noise_meter.svg)](https://pub.dartlang.org/packages/noise_meter) | diff --git a/packages/app_usage/example/.flutter-plugins-dependencies b/packages/app_usage/example/.flutter-plugins-dependencies index 8869a5276..72bea7d1c 100644 --- a/packages/app_usage/example/.flutter-plugins-dependencies +++ b/packages/app_usage/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[],"android":[{"name":"app_usage","path":"/Users/hoffmatteo/Desktop/CACHET/flutter-plugins/packages/app_usage/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"app_usage","dependencies":[]}],"date_created":"2023-07-20 21:35:52.362642","version":"3.10.5"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[],"android":[{"name":"app_usage","path":"/Users/arata/Developer/carp/flutter-plugins/packages/app_usage/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"app_usage","dependencies":[]}],"date_created":"2024-10-30 13:38:21.146381","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 0d464dcb2..704d1cd24 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,76 @@ +## 12.0.1 + +* Update of API and README doc +* Fix [#1118](https://github.com/cph-cachet/flutter-plugins/issues/1118) + +## 12.0.0 + +* **BREAKING** This release introduces a significant architectural change to the `health` plugin by removing the `singleton` pattern. + * **Dependency Injection for `DeviceInfoPlugin`**: + * The `Health` class is no longer a singleton. + * The `Health()` factory constructor is removed. + * The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests. + * This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance. + * **Impact**: + * For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). +* **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. + * For `WORKOUT`s that require above permissions, now those need to be requested manually. + * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055) +* Add `LEAN_BODY_MASS` data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097) + * The following AndroidManifest values are required to READ/WRITE `LEAN_BODY_MASS`: + + ```XML + + + ``` + +* iOS: Add `WATER_TEMPERATURE` and `UNDERWATER_DEPTH` health values [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) +* iOS: Add support for `Underwater Diving` workout [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096) +* Fix [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074) +* Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104) +* Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103) +* Fix [#1047](https://github.com/cph-cachet/flutter-plugins/issues/1047) and [#939](https://github.com/cph-cachet/flutter-plugins/issues/939) - PR [#1091](https://github.com/cph-cachet/flutter-plugins/pull/1091) +* Fix issue where `SLEEP_LIGHT` type was not aligned correctly - PR [#1086](https://github.com/cph-cachet/flutter-plugins/pull/1086) +* Fix [#1051](https://github.com/cph-cachet/flutter-plugins/issues/1051) - PR [#1052](https://github.com/cph-cachet/flutter-plugins/pull/1052) +* Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092) +* Updated `device_info_plus` to ^11.2.0 +* Example app: Updated `permission_handler` to ^11.3.1 + +## 11.1.1 + +* Fix of [#1059](https://github.com/cph-cachet/flutter-plugins/issues/1059) + +## 11.1.0 + +* Fix of [#1043](https://github.com/cph-cachet/flutter-plugins/issues/1043) +* Type-safe JSON deserialization using carp_serializable v. 2.0 + +## 11.0.0 + +* **BREAKING** Remove Google Fit support in the Android code, as well as Google FIt related dependencies and references throughout the documentation + * Remove `useHealthConnectIfAvailable` from the parameters of `Health().configure()` + * Remove the `disconnect` method which was previously used to disconnect from Google Fit. + * Remove the `flowRate` value from `writeBloodOxygen` as this is not supported by Health Connect. + * Remove support for various `HealthWorkoutActivityType`s which were supported by Google Fit. Some of these do not have suitable alternatives in Google Health Connect (and are not supported on iOS). The list of removed types can be found in PR [#1014](https://github.com/cph-cachet/flutter-plugins/pull/1014) +* **BREAKING** introduce a new `RecordingMethod` enum + * This can be used to filter records by automatic or manual entries when fetching data + * You can also specify the recording method to write in the metadata + * Remove `isManualEntry` from `HealthDataPoint` in favor of `recordingMethod`, of which the value is an enum `RecordingMethod` + * Remove `includeManualEntry` (previously a boolean) from some of the querying methods in favor of `recordingMethodsToFilter`. + * For complete details on relevant changes, see the description of PR [#1023](https://github.com/cph-cachet/flutter-plugins/pull/1023) +* Add support for all sleep stages across iOS and Android + * Clean up relevant documentation + * Remove undocumented sleep stages + * **BREAKING** certain sleep stages were removed/combined into other related stages see PR [#1026](https://github.com/cph-cachet/flutter-plugins/pull/1026) for the complete list of changes and a discussion of the motivation in issue [#985](https://github.com/cph-cachet/flutter-plugins/issues/985) +* Android: Add support for `OTHER` workout type +* Cleaned up workout activity types for consistency across iOS and Android, see PR [#1020](https://github.com/cph-cachet/flutter-plugins/pull/1020) for a complete list of changes +* iOS: add support for menstruation flow, PR [#1008](https://github.com/cph-cachet/flutter-plugins/pull/1008) +* Android: Add support for heart rate variability, PR [#1009](https://github.com/cph-cachet/flutter-plugins/pull/1009) +* iOS: add support for atrial fibrillation burden, PR [#1031](https://github.com/cph-cachet/flutter-plugins/pull/1031) +* Add support for UUIDs in health records for both HealthKit and Health Connect, PR [#1019](https://github.com/cph-cachet/flutter-plugins/pull/1019) +* Fix an issue when querying workouts, the native code could respond with an activity that is not supported in the Health package, causing an error - this will fallback to `HealthWorkoutActivityType.other` - PR [#1016](https://github.com/cph-cachet/flutter-plugins/pull/1016) +* Remove deprecated Android v1 embeddings, PR [#1021](https://github.com/cph-cachet/flutter-plugins/pull/1021) + ## 10.2.0 * Using named parameters in most methods for consistency. diff --git a/packages/health/LICENSE b/packages/health/LICENSE index 7dfb95c95..0edc55d82 100644 --- a/packages/health/LICENSE +++ b/packages/health/LICENSE @@ -1,17 +1,9 @@ MIT License. -Copyright 2019 Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). +Copyright 2020 the Technical University of Denmark (DTU). -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the ”Software”), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ”Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/health/README.md b/packages/health/README.md index 30f87c6ca..ffa41fc89 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -1,8 +1,8 @@ # Health -Enables reading and writing health data from/to Apple Health, Google Fit and Health Connect. +Enables reading and writing health data from/to [Apple Health](https://www.apple.com/health/) and [Google Health Connect](https://health.google/health-connect-android/). -> Google Fitness API is deprecated and will be turned down in 2024, thus this package will also transition to only support Health Connect. +> **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2024** developers cannot sign up for using the API. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. The plugin supports: @@ -17,7 +17,7 @@ The plugin supports: - cleaning up duplicate data points via the `removeDuplicates` method. - removing data of a given type in a selected period of time using the `delete` method. -Note that for Android, the target phone **needs** to have [Google Fit](https://www.google.com/fit/) or [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. +Note that for Android, the target phone **needs** to have the [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata&hl=en) app installed (which is currently in beta) and have access to the internet. See the tables below for supported health and workout data types. @@ -25,7 +25,7 @@ See the tables below for supported health and workout data types. ### Apple Health (iOS) -Step 1: Append the `Info.plist` with the following 2 entries +First, add the following 2 entries to the `Info.plist`: ```xml NSHealthShareUsageDescription @@ -34,59 +34,9 @@ Step 1: Append the `Info.plist` with the following 2 entries We will sync your data with the Apple Health app to give you better insights ``` -Step 2: Open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. +Then, open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. -### Android - -Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission. To set it add the following line to your `AndroidManifest.xml` file. - -```xml - -``` - -Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. - -```xml - - -``` - -#### Google Fit (Android option 1) - -Follow the guide at . Below is an example of following the guide. - -Change directory to your key-store directory (MacOS): - -`cd ~/.android/` - -Get your keystore SHA1 fingerprint: - -`keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android` - -Example output: - -```bash -Alias name: androiddebugkey -Creation date: Jan 01, 2013 -Entry type: PrivateKeyEntry -Certificate chain length: 1 -Certificate[1]: -Owner: CN=Android Debug, O=Android, C=US -Issuer: CN=Android Debug, O=Android, C=US -Serial number: 4aa9b300 -Valid from: Mon Jan 01 08:04:04 UTC 2013 until: Mon Jan 01 18:04:04 PST 2033 -Certificate fingerprints: - MD5: AE:9F:95:D0:A6:86:89:BC:A8:70:BA:34:FF:6A:AC:F9 - SHA1: BB:0D:AC:74:D3:21:E1:43:07:71:9B:62:90:AF:A1:66:6E:44:5D:75 - Signature algorithm name: SHA1withRSA - Version: 3 -``` - -Follow the instructions at for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential. - -The client id will look something like `YOUR_CLIENT_ID.apps.googleusercontent.com`. - -#### Health Connect (Android option 2) +### Google Health Connect (Android) Health Connect requires the following lines in the `AndroidManifest.xml` file (see also the example app): @@ -115,17 +65,38 @@ In the Health Connect permissions activity there is a link to your privacy polic ``` -If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: +For each data type you want to access, the READ and WRITE permissions need to be added to the `AndroidManifest.xml` file. The list of [permissions](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types#permissions) can be found here on the [data types](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types) page. -Example shown here (can also be found in the example app): +An example of asking for permission to read and write heart rate data is shown below and more examples can also be found in the example app. ```xml -... ``` -Furthermore, an `intent-filter` needs to be added to the `.MainActivity` activity. +Accessing fitness data (e.g. Steps) requires permission to access the "Activity Recognition" API. To set it add the following line to your `AndroidManifest.xml` file. + +```xml + +``` + +Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. + +```xml + + +``` + +Because this is labeled as a `dangerous` protection level, the permission system will not grant it automatically and it requires the user's action. +You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin. +Follow the plugin setup instructions and add the following line before requesting the data: + +```dart +await Permission.activityRecognition.request(); +await Permission.location.request(); +``` + +Finally, an `intent-filter` needs to be added to the `.MainActivity` activity. ```xml healthData = await Health().getHealthDataFromTypes( + List healthData = await health.getHealthDataFromTypes( now.subtract(Duration(days: 1)), now, types); // request permissions to write steps and blood glucose @@ -213,15 +179,20 @@ Below is a simplified flow of how to use the plugin. HealthDataAccess.READ_WRITE, HealthDataAccess.READ_WRITE ]; - await Health().requestAuthorization(types, permissions: permissions); + await health.requestAuthorization(types, permissions: permissions); // write steps and blood glucose - bool success = await Health().writeHealthData(10, HealthDataType.STEPS, now, now); - success = await Health().writeHealthData(3.1, HealthDataType.BLOOD_GLUCOSE, now, now); + bool success = await health.writeHealthData(10, HealthDataType.STEPS, now, now); + success = await health.writeHealthData(3.1, HealthDataType.BLOOD_GLUCOSE, now, now); + + // you can also specify the recording method to store in the metadata (default is RecordingMethod.automatic) + // on iOS only `RecordingMethod.automatic` and `RecordingMethod.manual` are supported + // Android additionally supports `RecordingMethod.active` and `RecordingMethod.unknown` + success &= await health.writeHealthData(10, HealthDataType.STEPS, now, now, recordingMethod: RecordingMethod.manual); // get the number of steps for today var midnight = DateTime(now.year, now.month, now.day); - int? steps = await Health().getTotalStepsInInterval(midnight, now); + int? steps = await health.getTotalStepsInInterval(midnight, now); ``` ### Health Data @@ -229,6 +200,7 @@ Below is a simplified flow of how to use the plugin. A [`HealthDataPoint`](https://pub.dev/documentation/health/latest/health/HealthDataPoint-class.html) object contains the following data fields: ```dart +String uuid; HealthValue value; HealthDataType type; HealthDataUnit unit; @@ -238,7 +210,7 @@ HealthPlatformType sourcePlatform; String sourceDeviceId; String sourceId; String sourceName; -bool isManualEntry; +RecordingMethod recordingMethod; WorkoutSummary? workoutSummary; ``` @@ -248,19 +220,32 @@ A `HealthDataPoint` object can be serialized to and from JSON using the `toJson( ```json { - "value": { - "__type": "NumericHealthValue", - "numeric_value": 141.0 - }, - "type": "STEPS", - "unit": "COUNT", - "date_from": "2024-04-03T10:06:57.736", - "date_to": "2024-04-03T10:12:51.724", - "source_platform": "appleHealth", - "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", - "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", - "source_name": "iPhone12.bardram.net", - "is_manual_entry": false + "value": { + "__type": "NumericHealthValue", + "numeric_value": 141.0 + }, + "type": "STEPS", + "unit": "COUNT", + "date_from": "2024-04-03T10:06:57.736", + "date_to": "2024-04-03T10:12:51.724", + "source_platform": "appleHealth", + "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", + "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", + "source_name": "iPhone12.bardram.net", + "recording_method": 3 + "value": { + "__type": "NumericHealthValue", + "numeric_value": 141.0 + }, + "type": "STEPS", + "unit": "COUNT", + "date_from": "2024-04-03T10:06:57.736", + "date_to": "2024-04-03T10:12:51.724", + "source_platform": "appleHealth", + "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", + "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", + "source_name": "iPhone12.bardram.net", + "recording_method": 2 } ``` @@ -275,6 +260,28 @@ flutter: Health Plugin Error: flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible})) ``` +### Filtering by recording method + +Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user. + +- Android provides an enum with 4 variations: +- iOS has a boolean value: + +As such, when fetching data you have the option to filter the fetched data by recording method as such: + +```dart +List healthData = await health.getHealthDataFromTypes( + types: types, + startTime: yesterday, + endTime: now, + recordingMethodsToFilter: [RecordingMethod.manual, RecordingMethod.unknown], +); +``` + +**Note that for this to work, the information needs to have been provided when writing the data to Health Connect or Apple Health**. For example, steps added manually through the Apple Health App will set `HKWasUserEntered` to true (corresponding to `RecordingMethod.manual`), however it seems that adding steps manually to Google Fit does not write the data with the `RecordingMethod.manual` in the metadata, instead it shows up as `RecordingMethod.unknown`. This is an open issue, and as such filtering manual entries when querying step count on Android with `getTotalStepsInInterval(includeManualEntries: false)` does not necessarily filter out manual steps. + +**NOTE**: On iOS, you can only filter by `RecordingMethod.automatic` and `RecordingMethod.manual` as it is stored `HKMetadataKeyWasUserEntered` is a boolean value in the metadata. + ### Filtering out duplicates If the same data is requested multiple times and saved in the same array duplicates will occur. @@ -291,204 +298,174 @@ If you have a list of data points, duplicates can be removed with: ```dart List points = ...; -points = Health().removeDuplicates(points); +points = health.removeDuplicates(points); ``` ## Data Types The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html). -| **Data Type** | **Unit** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| --------------------------- | ----------------------- | ------- | ----------------------- |---------------------------| -------------------------------------- | -| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | yes | | -| BASAL_ENERGY_BURNED | CALORIES | yes | | yes | | -| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | yes | | -| BLOOD_OXYGEN | PERCENTAGE | yes | yes | yes | | -| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | yes | | -| BODY_MASS_INDEX | NO_UNIT | yes | yes | yes | | -| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | yes | | -| BODY_WATER_MASS | KILOGRAMS | | | yes | | -| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | | -| HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | -| HEIGHT | METERS | yes | yes | yes | | -| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | | yes | | -| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | | -| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | | -| STEPS | COUNT | yes | yes | yes | | -| WAIST_CIRCUMFERENCE | METERS | yes | | | | -| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | | -| WEIGHT | KILOGRAMS | yes | yes | yes | | -| DISTANCE_WALKING_RUNNING | METERS | yes | | | | -| FLIGHTS_CLIMBED | COUNT | yes | | yes | | -| MOVE_MINUTES | MINUTES | | yes | | | -| DISTANCE_DELTA | METERS | | yes | yes | | -| MINDFULNESS | MINUTES | yes | | | | -| SLEEP_IN_BED | MINUTES | yes | | | | -| SLEEP_ASLEEP | MINUTES | yes | | yes | | -| SLEEP_AWAKE | MINUTES | yes | | yes | | -| SLEEP_DEEP | MINUTES | yes | | yes | | -| SLEEP_LIGHT | MINUTES | | | yes | | -| SLEEP_REM | MINUTES | yes | | yes | | -| SLEEP_OUT_OF_BED | MINUTES | | | yes | | -| SLEEP_SESSION | MINUTES | | | yes | | -| WATER | LITER | yes | yes | yes | | -| EXERCISE_TIME | MINUTES | yes | | | | -| WORKOUT | NO_UNIT | yes | yes | yes | See table below | -| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | | Requires Apple Watch to write the data | -| HEADACHE_NOT_PRESENT | MINUTES | yes | | | | -| HEADACHE_MILD | MINUTES | yes | | | | -| HEADACHE_MODERATE | MINUTES | yes | | | | -| HEADACHE_SEVERE | MINUTES | yes | | | | -| HEADACHE_UNSPECIFIED | MINUTES | yes | | | | -| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | -| ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | -| NUTRITION | NO_UNIT | yes | yes | yes | | +| **Data Type** | **Unit** | **Apple Health** | **Google Health Connect** | **Comments** | +| ---------------------------- | ----------------------- | ---------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | | +| ATRIAL_FIBRILLATION_BURDEN | PERCENTAGE | yes | | | +| BASAL_ENERGY_BURNED | CALORIES | yes | yes | | +| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | | +| BLOOD_OXYGEN | PERCENTAGE | yes | yes | | +| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | | +| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | | +| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | | +| BODY_MASS_INDEX | NO_UNIT | yes | yes | | +| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | | +| BODY_WATER_MASS | KILOGRAMS | | yes | | +| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | +| HEART_RATE | BEATS_PER_MINUTE | yes | yes | | +| HEIGHT | METERS | yes | yes | | +| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | yes | | +| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | yes | | +| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | +| STEPS | COUNT | yes | yes | | +| WAIST_CIRCUMFERENCE | METERS | yes | | | +| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | +| WEIGHT | KILOGRAMS | yes | yes | | +| DISTANCE_WALKING_RUNNING | METERS | yes | | | +| FLIGHTS_CLIMBED | COUNT | yes | yes | | +| DISTANCE_DELTA | METERS | | yes | | +| MINDFULNESS | MINUTES | yes | | | +| SLEEP_ASLEEP | MINUTES | yes | yes | on iOS, this refers to asleepUnspecified, and on Android this refers to STAGE_TYPE_SLEEPING (asleep but specific stage is unknown) | +| SLEEP_AWAKE | MINUTES | yes | yes | | +| SLEEP_AWAKE_IN_BED | MINUTES | | yes | | +| SLEEP_DEEP | MINUTES | yes | yes | | +| SLEEP_IN_BED | MINUTES | yes | | | +| SLEEP_LIGHT | MINUTES | yes | yes | on iOS, this refers to asleepCore | +| SLEEP_OUT_OF_BED | MINUTES | | yes | | +| SLEEP_REM | MINUTES | yes | yes | | +| SLEEP_UNKNOWN | MINUTES | | yes | | +| SLEEP_SESSION | MINUTES | | yes | | +| WATER | LITER | yes | yes | | +| EXERCISE_TIME | MINUTES | yes | | | +| WORKOUT | NO_UNIT | yes | yes | See table below | +| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| HEART_RATE_VARIABILITY_RMSSD | MILLISECONDS | | yes | | +| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | Requires Apple Watch to write the data | +| HEADACHE_NOT_PRESENT | MINUTES | yes | | | +| HEADACHE_MILD | MINUTES | yes | | | +| HEADACHE_MODERATE | MINUTES | yes | | | +| HEADACHE_SEVERE | MINUTES | yes | | | +| HEADACHE_UNSPECIFIED | MINUTES | yes | | | +| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | +| ELECTROCARDIOGRAM | VOLT | yes | | Requires Apple Watch to write the data | +| NUTRITION | NO_UNIT | yes | yes | | +| INSULIN_DELIVERY | INTERNATIONAL_UNIT | yes | | | +| MENSTRUATION_FLOW | NO_UNIT | yes | yes | | +| WATER_TEMPERATURE | DEGREE_CELSIUS | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | +| UNDERWATER_DEPTH | METER | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout | +| LEAN_BODY_MASS | KILOGRAMS | yes | yes | | ## Workout Types The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/documentation/health/latest/health/HealthWorkoutActivityType.html). -| **Workout Type** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| -------------------------------- | ------- | ----------------------- | ---------------------------- | ----------------------------------------------------------------- | -| ARCHERY | yes | yes | | | -| BADMINTON | yes | yes | yes | | -| BASEBALL | yes | yes | yes | | -| BASKETBALL | yes | yes | yes | | -| BIKING | yes | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | -| BOXING | yes | yes | yes | | -| CRICKET | yes | yes | yes | | -| CURLING | yes | yes | | | -| ELLIPTICAL | yes | yes | yes | | -| FENCING | yes | yes | yes | | -| AMERICAN_FOOTBALL | yes | yes | yes | | -| AUSTRALIAN_FOOTBALL | yes | yes | yes | | -| SOCCER | yes | yes | | | -| GOLF | yes | yes | yes | | -| GYMNASTICS | yes | yes | yes | | -| HANDBALL | yes | yes | yes | | -| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | yes | | -| HIKING | yes | yes | yes | | -| HOCKEY | yes | yes | | | -| SKATING | yes | yes | yes | On iOS this is skating_sports | -| JUMP_ROPE | yes | yes | | | -| KICKBOXING | yes | yes | | | -| MARTIAL_ARTS | yes | yes | yes | | -| PILATES | yes | yes | yes | | -| RACQUETBALL | yes | yes | yes | | -| RUGBY | yes | yes | yes | | -| RUNNING | yes | yes | yes | | -| ROWING | yes | yes | yes | | -| SAILING | yes | yes | yes | | -| CROSS_COUNTRY_SKIING | yes | yes | | | -| DOWNHILL_SKIING | yes | yes | | | -| SNOWBOARDING | yes | yes | yes | | -| SOFTBALL | yes | yes | yes | | -| SQUASH | yes | yes | yes | | -| STAIR_CLIMBING | yes | yes | yes | | -| SWIMMING | yes | yes | | | -| TABLE_TENNIS | yes | yes | yes | | -| TENNIS | yes | yes | yes | | -| VOLLEYBALL | yes | yes | yes | | -| WALKING | yes | yes | yes | | -| WATER_POLO | yes | yes | yes | | -| YOGA | yes | yes | yes | | -| BOWLING | yes | | | | -| CROSS_TRAINING | yes | | | | -| TRACK_AND_FIELD | yes | | | | -| DISC_SPORTS | yes | | | | -| LACROSSE | yes | | | | -| PREPARATION_AND_RECOVERY | yes | | | | -| FLEXIBILITY | yes | | | | -| COOLDOWN | yes | | | | -| WHEELCHAIR_WALK_PACE | yes | | | | -| WHEELCHAIR_RUN_PACE | yes | | | | -| HAND_CYCLING | yes | | | | -| CORE_TRAINING | yes | | | | -| FUNCTIONAL_STRENGTH_TRAINING | yes | | | | -| TRADITIONAL_STRENGTH_TRAINING | yes | | | | -| MIXED_CARDIO | yes | | | | -| STAIRS | yes | | | | -| STEP_TRAINING | yes | | | | -| FITNESS_GAMING | yes | | | | -| BARRE | yes | | | | -| CARDIO_DANCE | yes | | | | -| SOCIAL_DANCE | yes | | | | -| MIND_AND_BODY | yes | | | | -| PICKLEBALL | yes | | | | -| CLIMBING | yes | | | | -| EQUESTRIAN_SPORTS | yes | | | | -| FISHING | yes | | | | -| HUNTING | yes | | | | -| PLAY | yes | | | | -| SNOW_SPORTS | yes | | | | -| PADDLE_SPORTS | yes | | | | -| SURFING_SPORTS | yes | | | | -| WATER_FITNESS | yes | | | | -| WATER_SPORTS | yes | | | | -| TAI_CHI | yes | | | | -| WRESTLING | yes | | | | -| AEROBICS | | yes | | | -| BIATHLON | | yes | | | -| CALISTHENICS | | yes | yes | | -| CIRCUIT_TRAINING | | yes | | | -| CROSS_FIT | | yes | | | -| DANCING | | yes | yes | | -| DIVING | | yes | | | -| ELEVATOR | | yes | | | -| ERGOMETER | | yes | | | -| ESCALATOR | | yes | | | -| FRISBEE_DISC | | yes | yes | | -| GARDENING | | yes | | | -| GUIDED_BREATHING | | yes | yes | | -| HORSEBACK_RIDING | | yes | | | -| HOUSEWORK | | yes | | | -| INTERVAL_TRAINING | | yes | | | -| IN_VEHICLE | | yes | | | -| KAYAKING | | yes | | | -| KETTLEBELL_TRAINING | | yes | | | -| KICK_SCOOTER | | yes | | | -| KITE_SURFING | | yes | | | -| MEDITATION | | yes | | | -| MIXED_MARTIAL_ARTS | | yes | | | -| P90X | | yes | | | -| PARAGLIDING | | yes | yes | | -| POLO | | yes | | | -| ROCK_CLIMBING | (yes) | yes | yes | on iOS this will be stored as CLIMBING | -| RUNNING_JOGGING | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_SAND | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_TREADMILL | (yes) | yes | yes | on iOS this will be stored as RUNNING | -| SCUBA_DIVING | | yes | yes | | -| SKATING_CROSS | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INDOOR | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INLINE | (yes) | yes | | on iOS this will be stored as SKATING | -| SKIING_BACK_COUNTRY | | yes | | | -| SKIING_KITE | | yes | | | -| SKIING_ROLLER | | yes | | | -| SLEDDING | | yes | | | -| STAIR_CLIMBING_MACHINE | | yes | yes | | -| STANDUP_PADDLEBOARDING | | yes | | | -| STILL | | yes | | | -| STRENGTH_TRAINING | | yes | yes | | -| SURFING | | yes | yes | | -| SWIMMING_OPEN_WATER | | yes | yes | | -| SWIMMING_POOL | | yes | yes | | -| TEAM_SPORTS | | yes | | | -| TILTING | | yes | | | -| TREADMILL | | yes | | | -| VOLLEYBALL_BEACH | | yes | | | -| VOLLEYBALL_INDOOR | | yes | | | -| WAKEBOARDING | | yes | | | -| WALKING_FITNESS | | yes | | | -| WALKING_NORDIC | | yes | | | -| WALKING_STROLLER | | yes | | | -| WALKING_TREADMILL | | yes | | | -| WEIGHTLIFTING | | yes | yes | | -| WHEELCHAIR | | yes | yes | | -| WINDSURFING | | yes | | | -| ZUMBA | | yes | | | -| OTHER | yes | yes | | | +| **Workout Type** | **Apple Health** | **Google Health Connect** | **Comments** | +| -------------------------------- | ---------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | +| AMERICAN_FOOTBALL | yes | yes | | +| ARCHERY | yes | | | +| AUSTRALIAN_FOOTBALL | yes | yes | | +| BADMINTON | yes | yes | | +| BARRE | yes | | | +| BASEBALL | yes | yes | | +| BASKETBALL | yes | yes | | +| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | +| BOWLING | yes | | | +| BOXING | yes | yes | | +| CALISTHENICS | | yes | | +| CARDIO_DANCE | yes | (yes) | on Android this will be stored as DANCING | +| CLIMBING | yes | | | +| COOLDOWN | yes | | | +| CORE_TRAINING | yes | | | +| CRICKET | yes | yes | | +| CROSS_COUNTRY_SKIING | yes | (yes) | on Android this will be stored as SKIING | +| CROSS_TRAINING | yes | | | +| CURLING | yes | | | +| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | +| DISC_SPORTS | yes | | | +| DOWNHILL_SKIING | yes | (yes) | on Android this will be stored as SKIING | +| ELLIPTICAL | yes | yes | | +| EQUESTRIAN_SPORTS | yes | | | +| FENCING | yes | yes | | +| FISHING | yes | | | +| FITNESS_GAMING | yes | | | +| FLEXIBILITY | yes | | | +| FRISBEE_DISC | | yes | | +| FUNCTIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING | +| GOLF | yes | yes | | +| GUIDED_BREATHING | | yes | | +| GYMNASTICS | yes | yes | | +| HAND_CYCLING | yes | | | +| HANDBALL | yes | yes | | +| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | | +| HIKING | yes | yes | | +| HOCKEY | yes | | | +| HUNTING | yes | | | +| JUMP_ROPE | yes | | | +| KICKBOXING | yes | | | +| LACROSSE | yes | | | +| MARTIAL_ARTS | yes | yes | | +| MIND_AND_BODY | yes | | | +| MIXED_CARDIO | yes | | | +| PADDLE_SPORTS | yes | | | +| PARAGLIDING | | yes | | +| PICKLEBALL | yes | | | +| PILATES | yes | yes | | +| PLAY | yes | | | +| PREPARATION_AND_RECOVERY | yes | | | +| RACQUETBALL | yes | yes | | +| ROCK_CLIMBING | (yes) | yes | on iOS this will be stored as CLIMBING | +| ROWING | yes | yes | | +| RUGBY | yes | yes | | +| RUNNING | yes | yes | | +| RUNNING_TREADMILL | (yes) | yes | on iOS this will be stored as RUNNING | +| SAILING | yes | yes | | +| SCUBA_DIVING | | yes | | +| SKATING | yes | yes | On iOS this will be stored as SKATING_SPORTS | +| SKIING | (yes) | yes | on iOS you have to choose between CROSS_COUNTRY_SKIING and DOWNHILL_SKIING | +| SNOW_SPORTS | yes | | | +| SNOWBOARDING | yes | yes | | +| SOCCER | yes | | | +| SOCIAL_DANCE | yes | (yes) | on Android this will be stored as DANCING | +| SOFTBALL | yes | yes | | +| SQUASH | yes | yes | | +| STAIR_CLIMBING | yes | yes | | +| STAIR_CLIMBING_MACHINE | | yes | | +| STAIRS | yes | | | +| STEP_TRAINING | yes | | | +| STRENGTH_TRAINING | (yes) | yes | on iOS you have to choose between FUNCTIONAL_STRENGTH_TRAINING or TRADITIONAL_STRENGTH_TRAINING | +| SURFING | yes | yes | on iOS this is SURFING_SPORTS, but name changed here to fit with Android | +| SWIMMING | yes | (yes) | on Android you have to choose between SWIMMING_OPEN_WATER and SWIMMING_POOL | +| SWIMMING_OPEN_WATER | (yes) | yes | on iOS this will be stored as SWIMMING | +| SWIMMING_POOL | (yes) | yes | on iOS this will be stored as SWIMMING | +| TABLE_TENNIS | yes | yes | | +| TAI_CHI | yes | | | +| TENNIS | yes | yes | | +| TRACK_AND_FIELD | yes | | | +| TRADITIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING | +| UNDERWATER_DIVING | yes | | | +| VOLLEYBALL | yes | yes | | +| WALKING | yes | yes | | +| WATER_FITNESS | yes | | | +| WATER_POLO | yes | yes | | +| WATER_SPORTS | yes | | | +| WEIGHTLIFTING | | yes | | +| WHEELCHAIR | (yes) | yes | on iOS you have to choose between WHEELCHAIR_RUN_PACE or WHEELCHAIR_WALK_PACE | +| WHEELCHAIR_RUN_PACE | yes | (yes) | on Android this will be stored as WHEELCHAIR | +| WHEELCHAIR_WALK_PACE | yes | (yes) | on Android this will be stored as WHEELCHAIR | +| WRESTLING | yes | | | +| YOGA | yes | yes | | +| OTHER | yes | yes | | + +## License + +This software is copyright (c) the [Technical University of Denmark (DTU)](https://www.dtu.dk) and is part of the [Copenhagen Research Platform](https://carp.cachet.dk/). +This software is available 'as-is' under a [MIT license](LICENSE). diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index ee0b26c7f..1f96a1fb1 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -54,10 +54,7 @@ dependencies { def composeBom = platform('androidx.compose:compose-bom:2022.10.00') implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation("com.google.android.gms:play-services-fitness:21.1.0") - implementation("com.google.android.gms:play-services-auth:20.2.0") - // The new health connect api implementation("androidx.health.connect:connect-client:1.1.0-alpha07") def fragment_version = "1.6.2" implementation "androidx.fragment:fragment-ktx:$fragment_version" diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 323c157c4..1d6ee83dc 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -3,15 +3,12 @@ package cachet.plugins.health import android.app.Activity import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri -import android.os.Build import android.os.Handler import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.annotation.NonNull -import androidx.core.content.ContextCompat import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController import androidx.health.connect.client.permission.HealthPermission @@ -21,25 +18,12 @@ import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER import androidx.health.connect.client.records.MealType.MEAL_TYPE_LUNCH import androidx.health.connect.client.records.MealType.MEAL_TYPE_SNACK import androidx.health.connect.client.records.MealType.MEAL_TYPE_UNKNOWN +import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.request.AggregateGroupByDurationRequest import androidx.health.connect.client.request.AggregateRequest import androidx.health.connect.client.request.ReadRecordsRequest import androidx.health.connect.client.time.TimeRangeFilter import androidx.health.connect.client.units.* -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.fitness.Fitness -import com.google.android.gms.fitness.FitnessActivities -import com.google.android.gms.fitness.FitnessOptions -import com.google.android.gms.fitness.data.* -import com.google.android.gms.fitness.request.DataDeleteRequest -import com.google.android.gms.fitness.request.DataReadRequest -import com.google.android.gms.fitness.request.SessionInsertRequest -import com.google.android.gms.fitness.request.SessionReadRequest -import com.google.android.gms.fitness.result.DataReadResponse -import com.google.android.gms.fitness.result.SessionReadResponse -import com.google.android.gms.tasks.OnFailureListener -import com.google.android.gms.tasks.OnSuccessListener import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -48,4011 +32,2577 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.ActivityResultListener -import io.flutter.plugin.common.PluginRegistry.Registrar import java.time.* import java.time.temporal.ChronoUnit import java.util.* import java.util.concurrent.* import kotlinx.coroutines.* -const val GOOGLE_FIT_PERMISSIONS_REQUEST_CODE = 1111 -const val HEALTH_CONNECT_RESULT_CODE = 16969 const val CHANNEL_NAME = "flutter_health" -const val MMOLL_2_MGDL = 18.0 // 1 mmoll= 18 mgdl -// The minimum android level that can use Health Connect -const val MIN_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1 +const val ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" +const val AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" +const val BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" +const val BLOOD_GLUCOSE = "BLOOD_GLUCOSE" +const val BLOOD_OXYGEN = "BLOOD_OXYGEN" +const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" +const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" +const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" +const val LEAN_BODY_MASS = "LEAN_BODY_MASS" +const val BODY_TEMPERATURE = "BODY_TEMPERATURE" +const val BODY_WATER_MASS = "BODY_WATER_MASS" +const val DISTANCE_DELTA = "DISTANCE_DELTA" +const val FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" +const val HEART_RATE = "HEART_RATE" +const val HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" +const val HEIGHT = "HEIGHT" +const val MENSTRUATION_FLOW = "MENSTRUATION_FLOW" +const val RESPIRATORY_RATE = "RESPIRATORY_RATE" +const val RESTING_HEART_RATE = "RESTING_HEART_RATE" +const val STEPS = "STEPS" +const val WATER = "WATER" +const val WEIGHT = "WEIGHT" + +const val BREAKFAST = "BREAKFAST" +const val DINNER = "DINNER" +const val LUNCH = "LUNCH" +const val MEAL_UNKNOWN = "UNKNOWN" +const val NUTRITION = "NUTRITION" +const val SLEEP_ASLEEP = "SLEEP_ASLEEP" +const val SLEEP_AWAKE = "SLEEP_AWAKE" +const val SLEEP_AWAKE_IN_BED = "SLEEP_AWAKE_IN_BED" +const val SLEEP_DEEP = "SLEEP_DEEP" +const val SLEEP_IN_BED = "SLEEP_IN_BED" +const val SLEEP_LIGHT = "SLEEP_LIGHT" +const val SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" +const val SLEEP_REM = "SLEEP_REM" +const val SLEEP_SESSION = "SLEEP_SESSION" +const val SLEEP_UNKNOWN = "SLEEP_UNKNOWN" +const val SNACK = "SNACK" +const val WORKOUT = "WORKOUT" + +const val TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" -class HealthPlugin(private var channel: MethodChannel? = null) : - MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { - private var mResult: Result? = null - private var handler: Handler? = null - private var activity: Activity? = null - private var context: Context? = null - private var threadPoolExecutor: ExecutorService? = null - private var useHealthConnectIfAvailable: Boolean = false - private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher>? = - null - private lateinit var healthConnectClient: HealthConnectClient - private lateinit var scope: CoroutineScope - - private var BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" - private var HEIGHT = "HEIGHT" - private var WEIGHT = "WEIGHT" - private var STEPS = "STEPS" - private var AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" - private var ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" - private var HEART_RATE = "HEART_RATE" - private var BODY_TEMPERATURE = "BODY_TEMPERATURE" - private var BODY_WATER_MASS = "BODY_WATER_MASS" - private var BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" - private var BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" - private var BLOOD_OXYGEN = "BLOOD_OXYGEN" - private var BLOOD_GLUCOSE = "BLOOD_GLUCOSE" - private var MOVE_MINUTES = "MOVE_MINUTES" - private var DISTANCE_DELTA = "DISTANCE_DELTA" - private var WATER = "WATER" - private var RESTING_HEART_RATE = "RESTING_HEART_RATE" - private var BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" - private var FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" - private var RESPIRATORY_RATE = "RESPIRATORY_RATE" - - // TODO support unknown? - private var SLEEP_ASLEEP = "SLEEP_ASLEEP" - private var SLEEP_AWAKE = "SLEEP_AWAKE" - private var SLEEP_IN_BED = "SLEEP_IN_BED" - private var SLEEP_SESSION = "SLEEP_SESSION" - private var SLEEP_LIGHT = "SLEEP_LIGHT" - private var SLEEP_DEEP = "SLEEP_DEEP" - private var SLEEP_REM = "SLEEP_REM" - private var SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" - private var WORKOUT = "WORKOUT" - private var NUTRITION = "NUTRITION" - private var BREAKFAST = "BREAKFAST" - private var LUNCH = "LUNCH" - private var DINNER = "DINNER" - private var SNACK = "SNACK" - private var MEAL_UNKNOWN = "UNKNOWN" - - private var TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" - - val workoutTypeMap = - mapOf( - "AEROBICS" to FitnessActivities.AEROBICS, - "AMERICAN_FOOTBALL" to FitnessActivities.FOOTBALL_AMERICAN, - "ARCHERY" to FitnessActivities.ARCHERY, - "AUSTRALIAN_FOOTBALL" to - FitnessActivities.FOOTBALL_AUSTRALIAN, - "BADMINTON" to FitnessActivities.BADMINTON, - "BASEBALL" to FitnessActivities.BASEBALL, - "BASKETBALL" to FitnessActivities.BASKETBALL, - "BIATHLON" to FitnessActivities.BIATHLON, - "BIKING" to FitnessActivities.BIKING, - "BIKING_HAND" to FitnessActivities.BIKING_HAND, - "BIKING_MOUNTAIN" to FitnessActivities.BIKING_MOUNTAIN, - "BIKING_ROAD" to FitnessActivities.BIKING_ROAD, - "BIKING_SPINNING" to FitnessActivities.BIKING_SPINNING, - "BIKING_STATIONARY" to FitnessActivities.BIKING_STATIONARY, - "BIKING_UTILITY" to FitnessActivities.BIKING_UTILITY, - "BOXING" to FitnessActivities.BOXING, - "CALISTHENICS" to FitnessActivities.CALISTHENICS, - "CIRCUIT_TRAINING" to FitnessActivities.CIRCUIT_TRAINING, - "CRICKET" to FitnessActivities.CRICKET, - "CROSS_COUNTRY_SKIING" to - FitnessActivities.SKIING_CROSS_COUNTRY, - "CROSS_FIT" to FitnessActivities.CROSSFIT, - "CURLING" to FitnessActivities.CURLING, - "DANCING" to FitnessActivities.DANCING, - "DIVING" to FitnessActivities.DIVING, - "DOWNHILL_SKIING" to FitnessActivities.SKIING_DOWNHILL, - "ELEVATOR" to FitnessActivities.ELEVATOR, - "ELLIPTICAL" to FitnessActivities.ELLIPTICAL, - "ERGOMETER" to FitnessActivities.ERGOMETER, - "ESCALATOR" to FitnessActivities.ESCALATOR, - "FENCING" to FitnessActivities.FENCING, - "FRISBEE_DISC" to FitnessActivities.FRISBEE_DISC, - "GARDENING" to FitnessActivities.GARDENING, - "GOLF" to FitnessActivities.GOLF, - "GUIDED_BREATHING" to FitnessActivities.GUIDED_BREATHING, - "GYMNASTICS" to FitnessActivities.GYMNASTICS, - "HANDBALL" to FitnessActivities.HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - FitnessActivities - .HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to FitnessActivities.HIKING, - "HOCKEY" to FitnessActivities.HOCKEY, - "HORSEBACK_RIDING" to FitnessActivities.HORSEBACK_RIDING, - "HOUSEWORK" to FitnessActivities.HOUSEWORK, - "IN_VEHICLE" to FitnessActivities.IN_VEHICLE, - "ICE_SKATING" to FitnessActivities.ICE_SKATING, - "INTERVAL_TRAINING" to FitnessActivities.INTERVAL_TRAINING, - "JUMP_ROPE" to FitnessActivities.JUMP_ROPE, - "KAYAKING" to FitnessActivities.KAYAKING, - "KETTLEBELL_TRAINING" to - FitnessActivities.KETTLEBELL_TRAINING, - "KICK_SCOOTER" to FitnessActivities.KICK_SCOOTER, - "KICKBOXING" to FitnessActivities.KICKBOXING, - "KITE_SURFING" to FitnessActivities.KITESURFING, - "MARTIAL_ARTS" to FitnessActivities.MARTIAL_ARTS, - "MEDITATION" to FitnessActivities.MEDITATION, - "MIXED_MARTIAL_ARTS" to - FitnessActivities.MIXED_MARTIAL_ARTS, - "P90X" to FitnessActivities.P90X, - "PARAGLIDING" to FitnessActivities.PARAGLIDING, - "PILATES" to FitnessActivities.PILATES, - "POLO" to FitnessActivities.POLO, - "RACQUETBALL" to FitnessActivities.RACQUETBALL, - "ROCK_CLIMBING" to FitnessActivities.ROCK_CLIMBING, - "ROWING" to FitnessActivities.ROWING, - "ROWING_MACHINE" to FitnessActivities.ROWING_MACHINE, - "RUGBY" to FitnessActivities.RUGBY, - "RUNNING_JOGGING" to FitnessActivities.RUNNING_JOGGING, - "RUNNING_SAND" to FitnessActivities.RUNNING_SAND, - "RUNNING_TREADMILL" to FitnessActivities.RUNNING_TREADMILL, - "RUNNING" to FitnessActivities.RUNNING, - "SAILING" to FitnessActivities.SAILING, - "SCUBA_DIVING" to FitnessActivities.SCUBA_DIVING, - "SKATING_CROSS" to FitnessActivities.SKATING_CROSS, - "SKATING_INDOOR" to FitnessActivities.SKATING_INDOOR, - "SKATING_INLINE" to FitnessActivities.SKATING_INLINE, - "SKATING" to FitnessActivities.SKATING, - "SKIING" to FitnessActivities.SKIING, - "SKIING_BACK_COUNTRY" to - FitnessActivities.SKIING_BACK_COUNTRY, - "SKIING_KITE" to FitnessActivities.SKIING_KITE, - "SKIING_ROLLER" to FitnessActivities.SKIING_ROLLER, - "SLEDDING" to FitnessActivities.SLEDDING, - "SNOWBOARDING" to FitnessActivities.SNOWBOARDING, - "SNOWMOBILE" to FitnessActivities.SNOWMOBILE, - "SNOWSHOEING" to FitnessActivities.SNOWSHOEING, - "SOCCER" to FitnessActivities.FOOTBALL_SOCCER, - "SOFTBALL" to FitnessActivities.SOFTBALL, - "SQUASH" to FitnessActivities.SQUASH, - "STAIR_CLIMBING_MACHINE" to - FitnessActivities.STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to FitnessActivities.STAIR_CLIMBING, - "STANDUP_PADDLEBOARDING" to - FitnessActivities.STANDUP_PADDLEBOARDING, - "STILL" to FitnessActivities.STILL, - "STRENGTH_TRAINING" to FitnessActivities.STRENGTH_TRAINING, - "SURFING" to FitnessActivities.SURFING, - "SWIMMING_OPEN_WATER" to - FitnessActivities.SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to FitnessActivities.SWIMMING_POOL, - "SWIMMING" to FitnessActivities.SWIMMING, - "TABLE_TENNIS" to FitnessActivities.TABLE_TENNIS, - "TEAM_SPORTS" to FitnessActivities.TEAM_SPORTS, - "TENNIS" to FitnessActivities.TENNIS, - "TILTING" to FitnessActivities.TILTING, - "VOLLEYBALL_BEACH" to FitnessActivities.VOLLEYBALL_BEACH, - "VOLLEYBALL_INDOOR" to FitnessActivities.VOLLEYBALL_INDOOR, - "VOLLEYBALL" to FitnessActivities.VOLLEYBALL, - "WAKEBOARDING" to FitnessActivities.WAKEBOARDING, - "WALKING_FITNESS" to FitnessActivities.WALKING_FITNESS, - "WALKING_PACED" to FitnessActivities.WALKING_PACED, - "WALKING_NORDIC" to FitnessActivities.WALKING_NORDIC, - "WALKING_STROLLER" to FitnessActivities.WALKING_STROLLER, - "WALKING_TREADMILL" to FitnessActivities.WALKING_TREADMILL, - "WALKING" to FitnessActivities.WALKING, - "WATER_POLO" to FitnessActivities.WATER_POLO, - "WEIGHTLIFTING" to FitnessActivities.WEIGHTLIFTING, - "WHEELCHAIR" to FitnessActivities.WHEELCHAIR, - "WINDSURFING" to FitnessActivities.WINDSURFING, - "YOGA" to FitnessActivities.YOGA, - "ZUMBA" to FitnessActivities.ZUMBA, - "OTHER" to FitnessActivities.OTHER, - ) - - // TODO: Update with new workout types when Health Connect becomes the standard. - val workoutTypeMapHealthConnect = - mapOf( - // "AEROBICS" to - // ExerciseSessionRecord.EXERCISE_TYPE_AEROBICS, - "AMERICAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AMERICAN, - // "ARCHERY" to ExerciseSessionRecord.EXERCISE_TYPE_ARCHERY, - "AUSTRALIAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, - "BADMINTON" to - ExerciseSessionRecord - .EXERCISE_TYPE_BADMINTON, - "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, - "BASKETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_BASKETBALL, - // "BIATHLON" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIATHLON, - "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, - // "BIKING_HAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_HAND, - // "BIKING_MOUNTAIN" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_MOUNTAIN, - // "BIKING_ROAD" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_ROAD, - // "BIKING_SPINNING" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_SPINNING, - // "BIKING_STATIONARY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, - // "BIKING_UTILITY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_UTILITY, - "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, - "CALISTHENICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_CALISTHENICS, - // "CIRCUIT_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_CIRCUIT_TRAINING, - "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, - // "CROSS_COUNTRY_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, - // "CROSS_FIT" to - // ExerciseSessionRecord.EXERCISE_TYPE_CROSSFIT, - // "CURLING" to ExerciseSessionRecord.EXERCISE_TYPE_CURLING, - "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, - // "DIVING" to ExerciseSessionRecord.EXERCISE_TYPE_DIVING, - // "DOWNHILL_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, - // "ELEVATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ELEVATOR, - "ELLIPTICAL" to - ExerciseSessionRecord - .EXERCISE_TYPE_ELLIPTICAL, - // "ERGOMETER" to - // ExerciseSessionRecord.EXERCISE_TYPE_ERGOMETER, - // "ESCALATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ESCALATOR, - "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, - "FRISBEE_DISC" to - ExerciseSessionRecord - .EXERCISE_TYPE_FRISBEE_DISC, - // "GARDENING" to - // ExerciseSessionRecord.EXERCISE_TYPE_GARDENING, - "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, - "GUIDED_BREATHING" to - ExerciseSessionRecord - .EXERCISE_TYPE_GUIDED_BREATHING, - "GYMNASTICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_GYMNASTICS, - "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, - // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, - // "HORSEBACK_RIDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_HORSEBACK_RIDING, - // "HOUSEWORK" to - // ExerciseSessionRecord.EXERCISE_TYPE_HOUSEWORK, - // "IN_VEHICLE" to - // ExerciseSessionRecord.EXERCISE_TYPE_IN_VEHICLE, - "ICE_SKATING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ICE_SKATING, - // "INTERVAL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_INTERVAL_TRAINING, - // "JUMP_ROPE" to - // ExerciseSessionRecord.EXERCISE_TYPE_JUMP_ROPE, - // "KAYAKING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KAYAKING, - // "KETTLEBELL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KETTLEBELL_TRAINING, - // "KICK_SCOOTER" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICK_SCOOTER, - // "KICKBOXING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICKBOXING, - // "KITE_SURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KITESURFING, - "MARTIAL_ARTS" to - ExerciseSessionRecord - .EXERCISE_TYPE_MARTIAL_ARTS, - // "MEDITATION" to - // ExerciseSessionRecord.EXERCISE_TYPE_MEDITATION, - // "MIXED_MARTIAL_ARTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_MIXED_MARTIAL_ARTS, - // "P90X" to ExerciseSessionRecord.EXERCISE_TYPE_P90X, - "PARAGLIDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_PARAGLIDING, - "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, - // "POLO" to ExerciseSessionRecord.EXERCISE_TYPE_POLO, - "RACQUETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RACQUETBALL, - "ROCK_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROCK_CLIMBING, - "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, - "ROWING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROWING_MACHINE, - "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, - // "RUNNING_JOGGING" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_JOGGING, - // "RUNNING_SAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_SAND, - "RUNNING_TREADMILL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RUNNING_TREADMILL, - "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, - "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, - "SCUBA_DIVING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SCUBA_DIVING, - // "SKATING_CROSS" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_CROSS, - // "SKATING_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INDOOR, - // "SKATING_INLINE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INLINE, - "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, - "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, - // "SKIING_BACK_COUNTRY" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_BACK_COUNTRY, - // "SKIING_KITE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_KITE, - // "SKIING_ROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_ROLLER, - // "SLEDDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SLEDDING, - "SNOWBOARDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWBOARDING, - // "SNOWMOBILE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SNOWMOBILE, - "SNOWSHOEING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWSHOEING, - // "SOCCER" to - // ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, - "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, - "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, - "STAIR_CLIMBING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING, - // "STANDUP_PADDLEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_STANDUP_PADDLEBOARDING, - // "STILL" to ExerciseSessionRecord.EXERCISE_TYPE_STILL, - "STRENGTH_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STRENGTH_TRAINING, - "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, - "SWIMMING_OPEN_WATER" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_POOL, - // "SWIMMING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING, - "TABLE_TENNIS" to - ExerciseSessionRecord - .EXERCISE_TYPE_TABLE_TENNIS, - // "TEAM_SPORTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_TEAM_SPORTS, - "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, - // "TILTING" to ExerciseSessionRecord.EXERCISE_TYPE_TILTING, - // "VOLLEYBALL_BEACH" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_BEACH, - // "VOLLEYBALL_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_INDOOR, - "VOLLEYBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_VOLLEYBALL, - // "WAKEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WAKEBOARDING, - // "WALKING_FITNESS" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_FITNESS, - // "WALKING_PACED" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_PACED, - // "WALKING_NORDIC" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_NORDIC, - // "WALKING_STROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_STROLLER, - // "WALKING_TREADMILL" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_TREADMILL, - "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, - "WATER_POLO" to - ExerciseSessionRecord - .EXERCISE_TYPE_WATER_POLO, - "WEIGHTLIFTING" to - ExerciseSessionRecord - .EXERCISE_TYPE_WEIGHTLIFTING, - "WHEELCHAIR" to - ExerciseSessionRecord - .EXERCISE_TYPE_WHEELCHAIR, - // "WINDSURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WINDSURFING, - "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, - // "ZUMBA" to ExerciseSessionRecord.EXERCISE_TYPE_ZUMBA, - // "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER, - ) - override fun onAttachedToEngine( - @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding - ) { - scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel?.setMethodCallHandler(this) - context = flutterPluginBinding.applicationContext - threadPoolExecutor = Executors.newFixedThreadPool(4) - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - flutterPluginBinding.applicationContext - ) - } +class HealthPlugin(private var channel: MethodChannel? = null) : + MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { + private var mResult: Result? = null + private var handler: Handler? = null + private var activity: Activity? = null + private var context: Context? = null + private var threadPoolExecutor: ExecutorService? = null + private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher>? = + null + private lateinit var healthConnectClient: HealthConnectClient + private lateinit var scope: CoroutineScope + private var isReplySubmitted = false + + + override fun onAttachedToEngine( + @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding + ) { + scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel?.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + threadPoolExecutor = Executors.newFixedThreadPool(4) + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + flutterPluginBinding.applicationContext + ) } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel = null - activity = null - threadPoolExecutor!!.shutdown() - threadPoolExecutor = null + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = null + activity = null + threadPoolExecutor!!.shutdown() + threadPoolExecutor = null + } + + override fun success(p0: Any?) { + handler?.post { mResult?.success(p0) } + } + + override fun notImplemented() { + handler?.post { mResult?.notImplemented() } + } + + override fun error( + errorCode: String, + errorMessage: String?, + errorDetails: Any?, + ) { + handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + return false + } + + /** Handle calls from the MethodChannel */ + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "installHealthConnect" -> installHealthConnect(call, result) + "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) + "hasPermissions" -> hasPermissions(call, result) + "requestAuthorization" -> requestAuthorization(call, result) + "revokePermissions" -> revokePermissions(call, result) + "getData" -> getData(call, result) + "getIntervalData" -> getIntervalData(call, result) + "writeData" -> writeData(call, result) + "delete" -> deleteData(call, result) + "getAggregateData" -> getAggregateData(call, result) + "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) + "writeWorkoutData" -> writeWorkoutData(call, result) + "writeBloodPressure" -> writeBloodPressure(call, result) + "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMenstruationFlow" -> writeMenstruationFlow(call, result) + "writeMeal" -> writeMeal(call, result) + else -> result.notImplemented() } + } - // This static function is optional and equivalent to onAttachedToEngine. It supports the - // old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be - // called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - companion object { - @Suppress("unused") - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) - val plugin = HealthPlugin(channel) - registrar.addActivityResultListener(plugin) - channel.setMethodCallHandler(plugin) - } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + if (channel == null) { + return } + binding.addActivityResultListener(this) + activity = binding.activity - override fun success(p0: Any?) { - handler?.post { mResult?.success(p0) } - } + val requestPermissionActivityContract = + PermissionController.createRequestPermissionResultContract() - override fun notImplemented() { - handler?.post { mResult?.notImplemented() } - } + healthConnectRequestPermissionsLauncher = + (activity as ComponentActivity).registerForActivityResult( + requestPermissionActivityContract + ) { granted -> onHealthConnectPermissionCallback(granted) } + } - override fun error( - errorCode: String, - errorMessage: String?, - errorDetails: Any?, - ) { - handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } - } + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == GOOGLE_FIT_PERMISSIONS_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - Log.i("FLUTTER_HEALTH", "Access Granted!") - mResult?.success(true) - } else if (resultCode == Activity.RESULT_CANCELED) { - Log.i("FLUTTER_HEALTH", "Access Denied!") - mResult?.success(false) - } - } - return false - } + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } - private fun onHealthConnectPermissionCallback(permissionGranted: Set) { - if (permissionGranted.isEmpty()) { - mResult?.success(false) - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") - } else { - mResult?.success(true) - Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") - } + override fun onDetachedFromActivity() { + if (channel == null) { + return } - - private fun keyToHealthDataType(type: String): DataType { - return when (type) { - BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE - HEIGHT -> DataType.TYPE_HEIGHT - WEIGHT -> DataType.TYPE_WEIGHT - STEPS -> DataType.TYPE_STEP_COUNT_DELTA - AGGREGATE_STEP_COUNT -> DataType.AGGREGATE_STEP_COUNT_DELTA - ACTIVE_ENERGY_BURNED -> DataType.TYPE_CALORIES_EXPENDED - HEART_RATE -> DataType.TYPE_HEART_RATE_BPM - BODY_TEMPERATURE -> HealthDataTypes.TYPE_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_PRESSURE_DIASTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_OXYGEN -> HealthDataTypes.TYPE_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthDataTypes.TYPE_BLOOD_GLUCOSE - MOVE_MINUTES -> DataType.TYPE_MOVE_MINUTES - DISTANCE_DELTA -> DataType.TYPE_DISTANCE_DELTA - WATER -> DataType.TYPE_HYDRATION - SLEEP_ASLEEP -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_AWAKE -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_IN_BED -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_LIGHT -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_REM -> DataType.TYPE_SLEEP_SEGMENT - SLEEP_DEEP -> DataType.TYPE_SLEEP_SEGMENT - WORKOUT -> DataType.TYPE_ACTIVITY_SEGMENT - NUTRITION -> DataType.TYPE_NUTRITION - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } + activity = null + healthConnectRequestPermissionsLauncher = null + } + + private var healthConnectAvailable = false + private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + + private fun checkAvailability() { + healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) + healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE + } + + private fun installHealthConnect(call: MethodCall, result: Result) { + val uriString = + "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding" + context!!.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setPackage("com.android.vending") + data = Uri.parse(uriString) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra("overlay", true) + putExtra("callerId", context!!.packageName) + } + ) + result.success(null) + } + + private fun onHealthConnectPermissionCallback(permissionGranted: Set) { + if (!isReplySubmitted) { + if (permissionGranted.isEmpty()) { + mResult?.success(false) + Log.i("FLUTTER_HEALTH", "Health Connect permissions were not granted! Make sure to declare the required permissions in the AndroidManifest.xml file.") + } else { + mResult?.success(true) + Log.i("FLUTTER_HEALTH", "${permissionGranted.size} Health Connect permissions were granted!") + + // log the permissions granted for debugging + Log.i("FLUTTER_HEALTH", "Permissions granted: $permissionGranted") + } + isReplySubmitted = true } - - private fun getField(type: String): Field { - return when (type) { - BODY_FAT_PERCENTAGE -> Field.FIELD_PERCENTAGE - HEIGHT -> Field.FIELD_HEIGHT - WEIGHT -> Field.FIELD_WEIGHT - STEPS -> Field.FIELD_STEPS - ACTIVE_ENERGY_BURNED -> Field.FIELD_CALORIES - HEART_RATE -> Field.FIELD_BPM - BODY_TEMPERATURE -> HealthFields.FIELD_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC - BLOOD_PRESSURE_DIASTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC - BLOOD_OXYGEN -> HealthFields.FIELD_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - MOVE_MINUTES -> Field.FIELD_DURATION - DISTANCE_DELTA -> Field.FIELD_DISTANCE - WATER -> Field.FIELD_VOLUME - SLEEP_ASLEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_AWAKE -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_IN_BED -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_LIGHT -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_REM -> Field.FIELD_SLEEP_SEGMENT_TYPE - SLEEP_DEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE - WORKOUT -> Field.FIELD_ACTIVITY - NUTRITION -> Field.FIELD_NUTRIENTS - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } + } + + /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ + private fun writeMeal(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) + val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) + val calories = call.argument("calories") + val protein = call.argument("protein") as Double? + val carbs = call.argument("carbs") as Double? + val fat = call.argument("fat") as Double? + val caffeine = call.argument("caffeine") as Double? + val vitaminA = call.argument("vitamin_a") as Double? + val b1Thiamine = call.argument("b1_thiamine") as Double? + val b2Riboflavin = call.argument("b2_riboflavin") as Double? + val b3Niacin = call.argument("b3_niacin") as Double? + val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? + val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? + val b7Biotin = call.argument("b7_biotin") as Double? + val b9Folate = call.argument("b9_folate") as Double? + val b12Cobalamin = call.argument("b12_cobalamin") as Double? + val vitaminC = call.argument("vitamin_c") as Double? + val vitaminD = call.argument("vitamin_d") as Double? + val vitaminE = call.argument("vitamin_e") as Double? + val vitaminK = call.argument("vitamin_k") as Double? + val calcium = call.argument("calcium") as Double? + val chloride = call.argument("chloride") as Double? + val cholesterol = call.argument("cholesterol") as Double? + // Choline is not yet supported by Health Connect + // val choline = call.argument("choline") as Double? + val chromium = call.argument("chromium") as Double? + val copper = call.argument("copper") as Double? + val fatUnsaturated = call.argument("fat_unsaturated") as Double? + val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? + val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? + val fatSaturated = call.argument("fat_saturated") as Double? + val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? + val fiber = call.argument("fiber") as Double? + val iodine = call.argument("iodine") as Double? + val iron = call.argument("iron") as Double? + val magnesium = call.argument("magnesium") as Double? + val manganese = call.argument("manganese") as Double? + val molybdenum = call.argument("molybdenum") as Double? + val phosphorus = call.argument("phosphorus") as Double? + val potassium = call.argument("potassium") as Double? + val selenium = call.argument("selenium") as Double? + val sodium = call.argument("sodium") as Double? + val sugar = call.argument("sugar") as Double? + // Water is not support on a food in Health Connect + // val water = call.argument("water") as Double? + val zinc = call.argument("zinc") as Double? + + val name = call.argument("name") + val mealType = call.argument("meal_type")!! + + scope.launch { + try { + val list = mutableListOf() + list.add( + NutritionRecord( + name = name, + energy = calories?.kilocalories, + totalCarbohydrate = carbs?.grams, + protein = protein?.grams, + totalFat = fat?.grams, + caffeine = caffeine?.grams, + vitaminA = vitaminA?.grams, + thiamin = b1Thiamine?.grams, + riboflavin = b2Riboflavin?.grams, + niacin = b3Niacin?.grams, + pantothenicAcid = b5PantothenicAcid?.grams, + vitaminB6 = b6Pyridoxine?.grams, + biotin = b7Biotin?.grams, + folate = b9Folate?.grams, + vitaminB12 = b12Cobalamin?.grams, + vitaminC = vitaminC?.grams, + vitaminD = vitaminD?.grams, + vitaminE = vitaminE?.grams, + vitaminK = vitaminK?.grams, + calcium = calcium?.grams, + chloride = chloride?.grams, + cholesterol = cholesterol?.grams, + chromium = chromium?.grams, + copper = copper?.grams, + unsaturatedFat = fatUnsaturated?.grams, + monounsaturatedFat = fatMonounsaturated?.grams, + polyunsaturatedFat = fatPolyunsaturated?.grams, + saturatedFat = fatSaturated?.grams, + transFat = fatTransMonoenoic?.grams, + dietaryFiber = fiber?.grams, + iodine = iodine?.grams, + iron = iron?.grams, + magnesium = magnesium?.grams, + manganese = manganese?.grams, + molybdenum = molybdenum?.grams, + phosphorus = phosphorus?.grams, + potassium = potassium?.grams, + selenium = selenium?.grams, + sodium = sodium?.grams, + sugar = sugar?.grams, + zinc = zinc?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = + mapMealTypeToType[ + mealType] + ?: MEAL_TYPE_UNKNOWN, + ), + ) + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Meal was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the meal", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - private fun isIntField(dataSource: DataSource, unit: Field): Boolean { - val dataPoint = DataPoint.builder(dataSource).build() - val value = dataPoint.getValue(unit) - return value.format == Field.FORMAT_INT32 + } + + /** + * Save menstrual flow data + */ + private fun writeMenstruationFlow(call: MethodCall, result: Result) { + writeData(call, result) + } + + /** + * Save the blood oxygen saturation + */ + private fun writeBloodOxygen(call: MethodCall, result: Result) { + writeData(call, result) + } + + private fun getIntervalData(call: MethodCall, result: Result) { + getAggregateData(call, result) + } + + /** + * Revokes access to Health Connect using `revokeAllPermissions`. + * + * Note: When using `revokePermissions` with Health Connect, the app must be completely killed + * for it to take effect. + */ + private fun revokePermissions(call: MethodCall, result: Result) { + scope.launch { + Log.i("Health", "Disabling Health Connect") + healthConnectClient.permissionController.revokeAllPermissions() } - - // / Extracts the (numeric) value from a Health Data Point - private fun getHealthDataValue(dataPoint: DataPoint, field: Field): Any { - val value = dataPoint.getValue(field) - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - return when (value.format) { - Field.FORMAT_FLOAT -> - if (!isGlucose) value.asFloat() - else value.asFloat() * MMOLL_2_MGDL - Field.FORMAT_INT32 -> value.asInt() - Field.FORMAT_STRING -> value.asString() - else -> Log.e("Unsupported format:", value.format.toString()) - } + result.success(true) + } + + private fun getTotalStepsInInterval(call: MethodCall, result: Result) { + val start = call.argument("startTime")!! + val end = call.argument("endTime")!! + val recordingMethodsToFilter = call.argument>("recordingMethodsToFilter")!! + + if (recordingMethodsToFilter.isEmpty()) { + getAggregatedStepCount(start, end, result) + } else { + getStepCountFiltered(start, end, recordingMethodsToFilter, result) } - - /** Delete records of the given type in the time range */ - private fun delete(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - deleteHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataDeleteRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .addDataType(dataType) - .deleteAllSessions() - .build() - - val fitnessOptions = typesBuilder.build() - - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .deleteData(dataSource) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset deleted successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error deleting the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) - } + } + + private fun getAggregatedStepCount(start: Long, end: Long, result: Result) { + val startInstant = Instant.ofEpochMilli(start) + val endInstant = Instant.ofEpochMilli(end) + scope.launch { + try { + val response = + healthConnectClient.aggregate( + AggregateRequest( + metrics = + setOf( + StepsRecord.COUNT_TOTAL, + ), + timeRangeFilter = + TimeRangeFilter.between( + startInstant, + endInstant + ), + ), + ) + // The result may be null if no data is available in the + // time range. + val stepsInInterval = + response[StepsRecord.COUNT_TOTAL] ?: 0L + + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $stepsInInterval steps" + ) + result.success(stepsInInterval) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } + } - /** Save a Blood Pressure measurement with systolic and diastolic values */ - private fun writeBloodPressure(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeBloodPressureHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val dataType = HealthDataTypes.TYPE_BLOOD_PRESSURE - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC, - systolic - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC, - diastolic - ) - .build() - - val dataPoint = builder - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Pressure added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood pressure data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } + /** get the step records manually and filter out manual entries **/ + private fun getStepCountFiltered(start: Long, end: Long, recordingMethodsToFilter: List, result: Result) { + scope.launch { + try { + val request = + ReadRecordsRequest( + recordType = StepsRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + Instant.ofEpochMilli(start), + Instant.ofEpochMilli(end) + ), + ) + val response = healthConnectClient.readRecords(request) + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + response.records + ) + val totalSteps = filteredRecords.sumOf { (it as StepsRecord).count.toInt() } + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $totalSteps steps (excluding manual entries)" + ) + result.success(totalSteps) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } - - private fun writeMealHC(call: MethodCall, result: Result) { - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val calories = call.argument("caloriesConsumed") - val carbs = call.argument("carbohydrates") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fatTotal") as Double? - val caffeine = call.argument("caffeine") as Double? - val name = call.argument("name") - val mealType = call.argument("mealType")!! - - scope.launch { - try { - val list = mutableListOf() - list.add( - NutritionRecord( - name = name, - energy = calories?.kilocalories, - totalCarbohydrate = carbs?.grams, - protein = protein?.grams, - totalFat = fat?.grams, - caffeine = caffeine?.grams, - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - mealType = - MapMealTypeToTypeHC[ - mealType] - ?: MEAL_TYPE_UNKNOWN, - ), - ) - healthConnectClient.insertRecords( - list, - ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Meal was successfully added!" - ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the meal", - ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } + } + + private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + context!! + ) + } + result.success(healthConnectStatus) + } + + /** Filter records by recording methods */ + private fun filterRecordsByRecordingMethods( + recordingMethodsToFilter: List, + records: List + ): List { + if (recordingMethodsToFilter.isEmpty()) { + return records } - /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ - private fun writeMeal(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeMealHC(call, result) - return - } - - if (context == null) { - result.success(false) - return - } - - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val calories = call.argument("caloriesConsumed") - val carbs = call.argument("carbohydrates") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fatTotal") as Double? - val name = call.argument("name") - val mealType = call.argument("mealType")!! - - val dataType = DataType.TYPE_NUTRITION - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val nutrients = mutableMapOf(Field.NUTRIENT_CALORIES to calories?.toFloat()) - - if (carbs != null) { - nutrients[Field.NUTRIENT_TOTAL_CARBS] = carbs.toFloat() - } - - if (protein != null) { - nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() - } - - if (fat != null) { - nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() - } - - val dataBuilder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField(Field.FIELD_NUTRIENTS, nutrients) - - if (name != null) { - dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) - } - - dataBuilder.setField( - Field.FIELD_MEAL_TYPE, - MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN + return records.filter { record -> + Log.i( + "FLUTTER_HEALTH", + "Filtering record with recording method ${record.metadata.recordingMethod}, filtering by $recordingMethodsToFilter. Result: ${recordingMethodsToFilter.contains(record.metadata.recordingMethod)}" + ) + return@filter !recordingMethodsToFilter.contains(record.metadata.recordingMethod) + } + } + + private fun hasPermissions(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" ) - - val dataPoint = dataBuilder.build() - - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Meal added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the meal data!" - ) - ) - } catch (e3: Exception) { - result.success(false) - } + result.success(false) + return + } + val access = permissions[i] + val dataType = mapToType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } } - - /** Save a data type in Google Fit */ - private fun writeData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } - - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - val dataPoint = - if (!isIntField(dataSource, field)) { - builder.setField( - field, - (if (!isGlucose) value - else - (value / - MMOLL_2_MGDL) - .toFloat()) - ) - .build() - } else { - builder.setField(field, value.toInt()).build() - } - - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) - } + scope.launch { + result.success( + healthConnectClient + .permissionController + .getGrantedPermissions() + .containsAll(permList), + ) } - - /** - * Save the blood oxygen saturation, in Google Fit with the supplemental flow rate, in - * HealthConnect without - */ - private fun writeBloodOxygen(call: MethodCall, result: Result) { - // Health Connect does not support supplemental flow rate, thus it is ignored - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - - if (context == null) { - result.success(false) - return - } - - val dataType = HealthDataTypes.TYPE_OXYGEN_SATURATION - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val saturation = call.argument("value")!! - val flowRate = call.argument("flowRate")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } - - builder.setField(HealthFields.FIELD_SUPPLEMENTAL_OXYGEN_FLOW_RATE, flowRate) - builder.setField(HealthFields.FIELD_OXYGEN_SATURATION, saturation) - - val dataPoint = builder.build() - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Oxygen added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood oxygen data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } + } + + /** + * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission + * type. + */ + private fun requestAuthorization(call: MethodCall, result: Result) { + if (context == null) { + result.success(false) + return } - /** Save a Workout session with options for distance and calories expended */ - private fun writeWorkoutData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeWorkoutHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val type = call.argument("activityType")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - - val activityType = getActivityType(type) - // Create the Activity Segment DataSource - val activitySegmentDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_ACTIVITY_SEGMENT) - .setStreamName("FLUTTER_HEALTH - Activity") - .setType(DataSource.TYPE_RAW) - .build() - // Create the Activity Segment - val activityDataPoint = - DataPoint.builder(activitySegmentDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setActivityField( - Field.FIELD_ACTIVITY, - activityType - ) - .build() - // Add DataPoint to DataSet - val activitySegments = - DataSet.builder(activitySegmentDataSource) - .add(activityDataPoint) - .build() - - // If distance is provided - var distanceDataSet: DataSet? = null - if (totalDistance != null) { - // Create a data source - val distanceDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_DISTANCE_DELTA) - .setStreamName("FLUTTER_HEALTH - Distance") - .setType(DataSource.TYPE_RAW) - .build() - - val distanceDataPoint = - DataPoint.builder(distanceDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_DISTANCE, - totalDistance.toFloat() - ) - .build() - // Create a data set - distanceDataSet = - DataSet.builder(distanceDataSource) - .add(distanceDataPoint) - .build() - } - // If energyBurned is provided - var energyDataSet: DataSet? = null - if (totalEnergyBurned != null) { - // Create a data source - val energyDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType( - DataType.TYPE_CALORIES_EXPENDED - ) - .setStreamName("FLUTTER_HEALTH - Calories") - .setType(DataSource.TYPE_RAW) - .build() - - val energyDataPoint = - DataPoint.builder(energyDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_CALORIES, - totalEnergyBurned.toFloat() - ) - .build() - // Create a data set - energyDataSet = - DataSet.builder(energyDataSource) - .add(energyDataPoint) - .build() - } - - // Finish session setup - val session = - Session.Builder() - .setName( - activityType - ) // TODO: Make a sensible name / allow user to set - // name - .setDescription("") - .setIdentifier(UUID.randomUUID().toString()) - .setActivity(activityType) - .setStartTime(startTime, TimeUnit.MILLISECONDS) - .setEndTime(endTime, TimeUnit.MILLISECONDS) - .build() - // Build a session and add the values provided - val sessionInsertRequestBuilder = - SessionInsertRequest.Builder() - .setSession(session) - .addDataSet(activitySegments) - if (totalDistance != null) { - sessionInsertRequestBuilder.addDataSet(distanceDataSet!!) - } - if (totalEnergyBurned != null) { - sessionInsertRequestBuilder.addDataSet(energyDataSet!!) - } - val insertRequest = sessionInsertRequestBuilder.build() - - val fitnessOptionsBuilder = - FitnessOptions.builder() - .addDataType( - DataType.TYPE_ACTIVITY_SEGMENT, - FitnessOptions.ACCESS_WRITE - ) - if (totalDistance != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_WRITE, - ) - } - if (totalEnergyBurned != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_WRITE, - ) - } - val fitnessOptions = fitnessOptionsBuilder.build() + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount, - ) - .insertSession(insertRequest) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Workout was successfully added!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the workout" - ) - ) - } catch (e: Exception) { - result.success(false) - } + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" + ) + result.success(false) + return + } + val access = permissions[i]!! + val dataType = mapToType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } } - - /** Get all datapoints of the DataType within the given time range */ - private fun getData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getHCData(call, result) - return - } - - if (context == null) { - result.success(null) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - - // Add special cases for accessing workouts or sleep data. - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } else if (dataType == DataType.TYPE_ACTIVITY_SEGMENT) { - typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_READ) - .addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_READ - ) - .addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_READ - ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - // Handle data types - when (dataType) { - DataType.TYPE_SLEEP_SEGMENT -> { - // request to the sessions for sleep data - val request = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeSleepSessions() - .build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(request) - .addOnSuccessListener( - threadPoolExecutor!!, - sleepDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the sleeping data!", - ), - ) - } - DataType.TYPE_ACTIVITY_SEGMENT -> { - val readRequest: SessionReadRequest - val readRequestBuilder = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeActivitySessions() - .read(dataType) - .read( - DataType.TYPE_CALORIES_EXPENDED - ) - - // If fine location is enabled, read distance data - if (ContextCompat.checkSelfPermission( - context!!.applicationContext, - android.Manifest.permission - .ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - ) { - readRequestBuilder.read(DataType.TYPE_DISTANCE_DELTA) - } - readRequest = readRequestBuilder.build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(readRequest) - .addOnSuccessListener( - threadPoolExecutor!!, - workoutDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the workout data!", - ), - ) - } - else -> { - Fitness.getHistoryClient( - context!!.applicationContext, - googleSignInAccount - ) - .readData( - DataReadRequest.Builder() - .read(dataType) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build(), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - dataHandler( - dataType, - field, - includeManualEntry, - result - ), - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the data!", - ), - ) - } - } + if (healthConnectRequestPermissionsLauncher == null) { + result.success(false) + Log.i("FLUTTER_HEALTH", "Permission launcher not found") + return } - private fun getIntervalData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getAggregateHCData(call, result) - return - } - - if (context == null) { - result.success(null) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val interval = call.argument("interval")!! - val includeManualEntry = call.argument("includeManualEntry")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) + // Store the result to be called in [onHealthConnectPermissionCallback] + mResult = result + isReplySubmitted = false + healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + } + + /** Get all datapoints of the DataType within the given time range */ + private fun getData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() + val recordingMethodsToFilter = call.argument>("recordingMethodsToFilter")!! + + Log.i( + "FLUTTER_HEALTH", + "Getting data for $dataType between $startTime and $endTime, filtering by $recordingMethodsToFilter" + ) + + scope.launch { + try { + mapToType[dataType]?.let { classType -> + val records = mutableListOf() + + // Set up the initial request to read health records with specified + // parameters + var request = + ReadRecordsRequest( + recordType = classType, + // Define the maximum amount of data + // that HealthConnect can return + // in a single request + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData( - DataReadRequest.Builder() - .aggregate(dataType) - .bucketByTime( - interval, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build() - ) - .addOnSuccessListener( - threadPoolExecutor!!, - intervalDataHandler( - dataType, - field, - includeManualEntry, - result - ) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the interval data!" - ) - ) - } + var response = healthConnectClient.readRecords(request) + var pageToken = response.pageToken + + // Add the records from the initial response to the records list + records.addAll(response.records) + + // Continue making requests and fetching records while there is a + // page token + while (!pageToken.isNullOrEmpty()) { + request = + ReadRecordsRequest( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + pageToken = pageToken + ) + response = healthConnectClient.readRecords(request) - private fun getAggregateData(call: MethodCall, result: Result) { - if (context == null) { - result.success(null) - return - } + pageToken = response.pageToken + records.addAll(response.records) + } - val types = call.argument>("dataTypeKeys")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val activitySegmentDuration = call.argument("activitySegmentDuration")!! - val includeManualEntry = call.argument("includeManualEntry")!! + // Workout needs distance and total calories burned too + if (dataType == WORKOUT) { + var filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) - val typesBuilder = FitnessOptions.builder() - for (type in types) { - val dataType = keyToHealthDataType(type) - typesBuilder.addDataType(dataType) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions + for (rec in filteredRecords) { + val record = rec as ExerciseSessionRecord + val distanceRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + DistanceRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), ) - - val readWorkoutsRequest = - DataReadRequest.Builder() - .bucketByActivitySegment( - activitySegmentDuration, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - - for (type in types) { - val dataType = keyToHealthDataType(type) - readWorkoutsRequest.aggregate(dataType) - } - - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData(readWorkoutsRequest.build()) - .addOnSuccessListener( - threadPoolExecutor!!, - aggregateDataHandler(includeManualEntry, result) + var totalDistance = 0.0 + for (distanceRec in distanceRequest.records) { + totalDistance += + distanceRec.distance + .inMeters + } + + val energyBurnedRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + TotalCaloriesBurnedRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the aggregate data!" - ) + var totalEnergyBurned = 0.0 + for (energyBurnedRec in + energyBurnedRequest.records) { + totalEnergyBurned += + energyBurnedRec.energy + .inKilocalories + } + + val stepRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + StepsRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime + ), + ), ) - } - - private fun dataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - // / Fetch all data points for the specified DataType - val dataSet = response.getDataSet(dataType) - /// For each data point, extract the contents and send them to Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource.streamName.contains( - "user_input" - ) - } - } - // For each data point, extract the contents and send them to Flutter, along with - // date and unit. - val healthData = - dataPoints.mapIndexed { _, dataPoint -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun errHandler(result: Result, addMessage: String) = - OnFailureListener { exception -> - Handler(context!!.mainLooper).run { result.success(null) } - Log.w("FLUTTER_HEALTH::ERROR", addMessage) - Log.w("FLUTTER_HEALTH::ERROR", exception.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", exception.stackTrace.toString()) - } - - private fun sleepDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Return sleep time in Minutes if requested ASLEEP data - if (type == SLEEP_ASLEEP) { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - - if (type == SLEEP_IN_BED) { - val dataSets = response.getDataSet(session) - - // If the sleep session has finer granularity - // sub-components, extract them: - if (dataSets.isNotEmpty()) { - for (dataSet in dataSets) { - for (dataPoint in - dataSet.dataPoints) { - // searching OUT OF BED data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() != - 3 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } - } - } else { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - } - - if (type == SLEEP_AWAKE) { - val dataSets = response.getDataSet(session) - for (dataSet in dataSets) { - for (dataPoint in dataSet.dataPoints) { - // searching SLEEP AWAKE data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() == - 1 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } + var totalSteps = 0.0 + for (stepRec in stepRequest.records) { + totalSteps += stepRec.count + } + + // val metadata = (rec as Record).metadata + // Add final datapoint + healthConnectData.add( + // mapOf( + mapOf( + "uuid" to record.metadata.id, + "workoutActivityType" to + (workoutTypeMap + .filterValues { + it == + record.exerciseType } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun intervalDataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - /// Fetch all data points for the specified DataType - // val dataSet = response.getDataSet(dataType) - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and send them to - // Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - for (field in dataPoint.dataType.fields) { - val healthDataItems = - dataPoints.mapIndexed { _, dataPoint - -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - "is_manual_entry" to - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - ) - } - healthData.addAll(healthDataItems) - } - } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun aggregateDataHandler(includeManualEntry: Boolean, result: Result) = - OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - var sourceName: Any = "" - var sourceId: Any = "" - var isManualEntry: Any = false - var totalSteps: Any = 0 - var totalDistance: Any = 0 - var totalEnergyBurned: Any = 0 - /// Fetch all data points for the specified DataType - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and - // send them to Flutter, - // along with date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { - _, - dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - sourceName = - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")) - sourceId = - dataPoint.originalDataSource - .streamIdentifier - isManualEntry = - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - for (field in dataPoint.dataType.fields) { - when (field) { - getField(STEPS) -> { - totalSteps = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - DISTANCE_DELTA - ) -> { - totalDistance = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - ACTIVE_ENERGY_BURNED - ) -> { - totalEnergyBurned = - getHealthDataValue( - dataPoint, - field - ) - } - } - } - } - } - val healthDataItems = - hashMapOf( - "value" to - bucket.getEndTime( - TimeUnit.MINUTES - ) - - bucket.getStartTime( - TimeUnit.MINUTES - ), - "date_from" to - bucket.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - bucket.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to sourceName, - "source_id" to sourceId, - "is_manual_entry" to - isManualEntry, - "workout_type" to - bucket.activity - .toLowerCase(), - "total_steps" to totalSteps, - "total_distance" to - totalDistance, - "total_energy_burned" to - totalEnergyBurned - ) - healthData.add(healthDataItems) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun workoutDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Look for calories and distance if they - var totalEnergyBurned = 0.0 - var totalDistance = 0.0 - for (dataSet in response.getDataSet(session)) { - if (dataSet.dataType == - DataType.TYPE_CALORIES_EXPENDED - ) { - for (dataPoint in dataSet.dataPoints) { - totalEnergyBurned += - dataPoint.getValue( - Field.FIELD_CALORIES - ) - .toString() - .toDouble() - } - } - if (dataSet.dataType == DataType.TYPE_DISTANCE_DELTA - ) { - for (dataPoint in dataSet.dataPoints) { - totalDistance += - dataPoint.getValue( - Field.FIELD_DISTANCE - ) - .toString() - .toDouble() - } - } - } - healthData.add( - hashMapOf( - "workoutActivityType" to - (workoutTypeMap - .filterValues { - it == - session.activity - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } + .keys + .firstOrNull() + ?: "OTHER"), + "totalDistance" to + if (totalDistance == + 0.0 + ) + null + else + totalDistance, + "totalDistanceUnit" to + "METER", + "totalEnergyBurned" to + if (totalEnergyBurned == + 0.0 + ) + null + else + totalEnergyBurned, + "totalEnergyBurnedUnit" to + "KILOCALORIE", + "totalSteps" to + if (totalSteps == + 0.0 + ) + null + else + totalSteps, + "totalStepsUnit" to + "COUNT", + "unit" to "MINUTES", + "date_from" to + rec.startTime + .toEpochMilli(), + "date_to" to + rec.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to + record.metadata + .dataOrigin + .packageName, + ), + ) } + // Filter sleep stages for requested stage + } else if (classType == SleepSessionRecord::class) { + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + response.records + ) - private fun callToHealthTypes(call: MethodCall): FitnessOptions { - val typesBuilder = FitnessOptions.builder() - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance() - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance() - - assert(types != null) - assert(permissions != null) - assert(types!!.count() == permissions!!.count()) - - for ((i, typeKey) in types.withIndex()) { - val access = permissions[i] - val dataType = keyToHealthDataType(typeKey) - when (access) { - 0 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_READ) - 1 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - 2 -> { - typesBuilder.addDataType( - dataType, - FitnessOptions.ACCESS_READ + for (rec in filteredRecords) { + if (rec is SleepSessionRecord) { + if (dataType == SLEEP_SESSION) { + healthConnectData.addAll( + convertRecord( + rec, + dataType ) - typesBuilder.addDataType( + ) + } else { + for (recStage in rec.stages) { + if (dataType == + mapSleepStageToType[ + recStage.stage] + ) { + healthConnectData + .addAll( + convertRecordStage( + recStage, dataType, - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - if (typeKey == SLEEP_ASLEEP || - typeKey == SLEEP_AWAKE || - typeKey == SLEEP_IN_BED - ) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - when (access) { - 0 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE + rec.metadata + ) ) } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) + } } + } } - if (typeKey == WORKOUT) { - when (access) { - 0 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } + } else { + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) + for (rec in filteredRecords) { + healthConnectData.addAll( + convertRecord(rec, dataType) + ) } + } } - return typesBuilder.build() - } - - private fun hasPermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - hasPermissionsHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - val optionsToRegister = callToHealthTypes(call) - - val isGranted = - GoogleSignIn.hasPermissions( - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, - ) - - result?.success(isGranted) + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } - - /** - * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission - * type. - */ - private fun requestAuthorization(call: MethodCall, result: Result) { - if (context == null) { - result.success(false) - return - } - mResult = result - - if (useHealthConnectIfAvailable && healthConnectAvailable) { - requestAuthorizationHC(call, result) - return - } - - val optionsToRegister = callToHealthTypes(call) - - // Set to false due to bug described in - // https://github.com/cph-cachet/flutter-plugins/issues/640#issuecomment-1366830132 - val isGranted = false - - // If not granted then ask for permission - if (!isGranted && activity != null) { - GoogleSignIn.requestPermissions( - activity!!, - GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, + } + + private fun convertRecordStage( + stage: SleepSessionRecord.Stage, + dataType: String, + metadata: Metadata + ): List> { + var sourceName = metadata.dataOrigin + .packageName + return listOf( + mapOf( + "uuid" to metadata.id, + "stage" to stage.stage, + "value" to + ChronoUnit.MINUTES.between( + stage.startTime, + stage.endTime + ), + "date_from" to stage.startTime.toEpochMilli(), + "date_to" to stage.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to sourceName, + ), + ) + } + + private fun getAggregateData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val interval = call.argument("interval")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() + scope.launch { + try { + mapToAggregateMetric[dataType]?.let { metricClassType -> + val request = + AggregateGroupByDurationRequest( + metrics = setOf(metricClassType), + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + timeRangeSlicer = + Duration.ofSeconds( + interval + ) ) - } else { // / Permission already granted - result?.success(true) - } - } + val response = healthConnectClient.aggregateGroupByDuration(request) + + for (durationResult in response) { + // The result may be null if no data is available in the + // time range + var totalValue = durationResult.result[metricClassType] + if (totalValue is Length) { + totalValue = totalValue.inMeters + } else if (totalValue is Energy) { + totalValue = totalValue.inKilocalories + } - /** - * Revokes access to Google Fit using the `disableFit`-method. - * - * Note: Using the `revokeAccess` creates a bug on android when trying to reapply for - * permissions afterwards, hence `disableFit` was used. - */ - private fun revokePermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - result.notImplemented() - return - } - if (context == null) { - result.success(false) - return - } - Fitness.getConfigClient( - activity!!, - GoogleSignIn.getLastSignedInAccount(context!!)!! - ) - .disableFit() - .addOnSuccessListener { - Log.i("Health", "Disabled Google Fit") - result.success(true) + val packageNames = + durationResult.result.dataOrigins + .joinToString { origin -> + origin.packageName } - .addOnFailureListener { e -> - Log.w( - "Health", - "There was an error disabling Google Fit", - e + + val data = + mapOf( + "value" to + (totalValue + ?: 0), + "date_from" to + durationResult.startTime + .toEpochMilli(), + "date_to" to + durationResult.endTime + .toEpochMilli(), + "source_name" to + packageNames, + "source_id" to "", + "is_manual_entry" to + packageNames.contains( + "user_input" ) - result.success(false) - } + ) + healthConnectData.add(data) + } + } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } + } - private fun getTotalStepsInInterval(call: MethodCall, result: Result) { - val start = call.argument("startTime")!! - val end = call.argument("endTime")!! + // TODO: Find alternative to SOURCE_ID or make it nullable? + private fun convertRecord(record: Any, dataType: String): List> { + val metadata = (record as Record).metadata + when (record) { + is WeightRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.weight + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getStepsHealthConnect(start, end, result) - return - } + is HeightRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.height + .inMeters, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val context = context ?: return + is BodyFatRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val stepsDataType = keyToHealthDataType(STEPS) - val aggregatedDataType = keyToHealthDataType(AGGREGATE_STEP_COUNT) + is LeanBodyMassRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.mass + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val fitnessOptions = - FitnessOptions.builder() - .addDataType(stepsDataType) - .addDataType(aggregatedDataType) - .build() - val gsa = GoogleSignIn.getAccountForExtension(context, fitnessOptions) + is StepsRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to record.count, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val ds = - DataSource.Builder() - .setAppPackageName("com.google.android.gms") - .setDataType(stepsDataType) - .setType(DataSource.TYPE_DERIVED) - .setStreamName("estimated_steps") - .build() + is ActiveCaloriesBurnedRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val duration = (end - start).toInt() + is HeartRateRecord -> + return record.samples.map { + mapOf( + "uuid" to + metadata.id, + "value" to it.beatsPerMinute, + "date_from" to + it.time.toEpochMilli(), + "date_to" to it.time.toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) + } + + is HeartRateVariabilityRmssdRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.heartRateVariabilityMillis, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val request = - DataReadRequest.Builder() - .aggregate(ds) - .bucketByTime(duration, TimeUnit.MILLISECONDS) - .setTimeRange(start, end, TimeUnit.MILLISECONDS) - .build() - - Fitness.getHistoryClient(context, gsa) - .readData(request) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the total steps in the interval!", - ), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - getStepsInRange( - start, - end, - aggregatedDataType, - result - ), - ) - } + is BodyTemperatureRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.temperature + .inCelsius, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - private fun getStepsHealthConnect(start: Long, end: Long, result: Result) = - scope.launch { - try { - val startInstant = Instant.ofEpochMilli(start) - val endInstant = Instant.ofEpochMilli(end) - val response = - healthConnectClient.aggregate( - AggregateRequest( - metrics = - setOf( - StepsRecord.COUNT_TOTAL - ), - timeRangeFilter = - TimeRangeFilter.between( - startInstant, - endInstant - ), - ), - ) - // The result may be null if no data is available in the - // time range. - val stepsInInterval = - response[StepsRecord.COUNT_TOTAL] ?: 0L - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "returning $stepsInInterval steps" - ) - result.success(stepsInInterval) - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "unable to return steps") - result.success(null) - } - } + is BodyWaterMassRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.mass + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - private fun getStepsInRange( - start: Long, - end: Long, - aggregatedDataType: DataType, - result: Result, - ) = OnSuccessListener { response: DataReadResponse -> - val map = HashMap() // need to return to Dart so can't use sparse array - for (bucket in response.buckets) { - val dp = bucket.dataSets.firstOrNull()?.dataPoints?.firstOrNull() - if (dp != null) { - val count = dp.getValue(aggregatedDataType.fields[0]) - - val startTime = dp.getStartTime(TimeUnit.MILLISECONDS) - val startDate = Date(startTime) - val endDate = Date(dp.getEndTime(TimeUnit.MILLISECONDS)) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "returning $count steps for $startDate - $endDate", + is BloodPressureRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + if (dataType == + BLOOD_PRESSURE_DIASTOLIC ) - map[startTime] = count.asInt() - } else { - val startDay = Date(start) - val endDay = Date(end) - Log.i("FLUTTER_HEALTH::ERROR", "no steps for $startDay - $endDay") - } - } - - assert(map.size <= 1) { - "getTotalStepsInInterval should return only one interval. Found: ${map.size}" - } - Handler(context!!.mainLooper).run { result.success(map.values.firstOrNull()) } - } - - /// Disconnect Google fit - private fun disconnect(call: MethodCall, result: Result) { - if (activity == null) { - result.success(false) - return - } - val context = activity!!.applicationContext - - val fitnessOptions = callToHealthTypes(call) - val googleAccount = GoogleSignIn.getAccountForExtension(context, fitnessOptions) - Fitness.getConfigClient(context, googleAccount).disableFit().continueWith { - val signinOption = - GoogleSignInOptions.Builder( - GoogleSignInOptions - .DEFAULT_SIGN_IN - ) - .requestId() - .requestEmail() - .build() - val googleSignInClient = GoogleSignIn.getClient(context, signinOption) - googleSignInClient.signOut() - result.success(true) - } - } - - private fun getActivityType(type: String): String { - return workoutTypeMap[type] ?: FitnessActivities.UNKNOWN - } + record.diastolic + .inMillimetersOfMercury + else + record.systolic + .inMillimetersOfMercury, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - /** Handle calls from the MethodChannel */ - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "installHealthConnect" -> installHealthConnect(call, result) - "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) - "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) - "hasPermissions" -> hasPermissions(call, result) - "requestAuthorization" -> requestAuthorization(call, result) - "revokePermissions" -> revokePermissions(call, result) - "getData" -> getData(call, result) - "getIntervalData" -> getIntervalData(call, result) - "writeData" -> writeData(call, result) - "delete" -> delete(call, result) - "getAggregateData" -> getAggregateData(call, result) - "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) - "writeWorkoutData" -> writeWorkoutData(call, result) - "writeBloodPressure" -> writeBloodPressure(call, result) - "writeBloodOxygen" -> writeBloodOxygen(call, result) - "writeMeal" -> writeMeal(call, result) - "disconnect" -> disconnect(call, result) - else -> result.notImplemented() - } - } + is OxygenSaturationRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - if (channel == null) { - return - } - binding.addActivityResultListener(this) - activity = binding.activity + is BloodGlucoseRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.level + .inMilligramsPerDeciliter, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - val requestPermissionActivityContract = - PermissionController.createRequestPermissionResultContract() + is DistanceRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.distance + .inMeters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - healthConnectRequestPermissionsLauncher = - (activity as ComponentActivity).registerForActivityResult( - requestPermissionActivityContract - ) { granted -> onHealthConnectPermissionCallback(granted) } - } + is HydrationRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.volume + .inLiters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } + is TotalCaloriesBurnedRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } + is BasalMetabolicRateRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.basalMetabolicRate + .inKilocaloriesPerDay, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - override fun onDetachedFromActivity() { - if (channel == null) { - return - } - activity = null - healthConnectRequestPermissionsLauncher = null - } + is SleepSessionRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "value" to + ChronoUnit.MINUTES + .between( + record.startTime, + record.endTime + ), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ), + ) - /** HEALTH CONNECT BELOW */ - var healthConnectAvailable = false - var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + is RestingHeartRateRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to + record.beatsPerMinute, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) + ) - fun checkAvailability() { - healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) - healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE - } + is FloorsClimbedRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to record.floors, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) + ) - private fun installHealthConnect(call: MethodCall, result: Result) { - val uriString = - "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding" - context!!.startActivity( - Intent(Intent.ACTION_VIEW).apply { - setPackage("com.android.vending") - data = Uri.parse(uriString) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - putExtra("overlay", true) - putExtra("callerId", context!!.packageName) - } + is RespiratoryRateRecord -> + return listOf( + mapOf( + "uuid" to + metadata.id, + "value" to record.rate, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) ) - result.success(null) - } - fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { - useHealthConnectIfAvailable = true - result.success(null) - } + is NutritionRecord -> + return listOf( + mapOf( + "uuid" to metadata.id, + "calories" to record.energy?.inKilocalories, + "protein" to record.protein?.inGrams, + "carbs" to record.totalCarbohydrate?.inGrams, + "fat" to record.totalFat?.inGrams, + "caffeine" to record.caffeine?.inGrams, + "vitamin_a" to record.vitaminA?.inGrams, + "b1_thiamine" to record.thiamin?.inGrams, + "b2_riboflavin" to record.riboflavin?.inGrams, + "b3_niacin" to record.niacin?.inGrams, + "b5_pantothenic_acid" to record.pantothenicAcid?.inGrams, + "b6_pyridoxine" to record.vitaminB6?.inGrams, + "b7_biotin" to record.biotin?.inGrams, + "b9_folate" to record.folate?.inGrams, + "b12_cobalamin" to record.vitaminB12?.inGrams, + "vitamin_c" to record.vitaminC?.inGrams, + "vitamin_d" to record.vitaminD?.inGrams, + "vitamin_e" to record.vitaminE?.inGrams, + "vitamin_k" to record.vitaminK?.inGrams, + "calcium" to record.calcium?.inGrams, + "chloride" to record.chloride?.inGrams, + "cholesterol" to record.cholesterol?.inGrams, + "choline" to null, + "chromium" to record.chromium?.inGrams, + "copper" to record.copper?.inGrams, + "fat_unsaturated" to record.unsaturatedFat?.inGrams, + "fat_monounsaturated" to record.monounsaturatedFat?.inGrams, + "fat_polyunsaturated" to record.polyunsaturatedFat?.inGrams, + "fat_saturated" to record.saturatedFat?.inGrams, + "fat_trans_monoenoic" to record.transFat?.inGrams, + "fiber" to record.dietaryFiber?.inGrams, + "iodine" to record.iodine?.inGrams, + "iron" to record.iron?.inGrams, + "magnesium" to record.magnesium?.inGrams, + "manganese" to record.manganese?.inGrams, + "molybdenum" to record.molybdenum?.inGrams, + "phosphorus" to record.phosphorus?.inGrams, + "potassium" to record.potassium?.inGrams, + "selenium" to record.selenium?.inGrams, + "sodium" to record.sodium?.inGrams, + "sugar" to record.sugar?.inGrams, + "water" to null, + "zinc" to record.zinc?.inGrams, + "name" to (record.name ?: ""), + "meal_type" to + (mapTypeToMealType[ + record.mealType] + ?: MEAL_TYPE_UNKNOWN), + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) + ) - private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - context!! - ) - } - result.success(healthConnectStatus) + is MenstruationFlowRecord -> + return listOf( + mapOf( + "uuid" to metadata.id, + "value" to record.flow, + "date_from" to record.time.toEpochMilli(), + "date_to" to record.time.toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + "recording_method" to + metadata.recordingMethod + ) + ) + // is ExerciseSessionRecord -> return listOf(mapOf("value" to , + // "date_from" to , + // "date_to" to , + // "source_id" to "", + // "source_name" to + // metadata.dataOrigin.packageName)) + else -> + throw IllegalArgumentException( + "Health data type not supported" + ) // TODO: Exception or error? } - - private fun hasPermissionsHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" + } + + // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should + // not adopt a single type with attached stages approach + private fun writeData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val value = call.argument("value")!! + val recordingMethod = call.argument("recordingMethod")!! + + Log.i( + "FLUTTER_HEALTH", + "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" + ) + + val record = + when (type) { + BODY_FAT_PERCENTAGE -> + BodyFatRecord( + time = + Instant.ofEpochMilli( + startTime + ), + percentage = + Percentage( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + LEAN_BODY_MASS -> + LeanBodyMassRecord( + time = + Instant.ofEpochMilli( + startTime + ), + mass = + Mass.kilograms( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + HEIGHT -> + HeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + height = + Length.meters( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + WEIGHT -> + WeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + weight = + Mass.kilograms( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + STEPS -> + StepsRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + count = value.toLong(), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + ACTIVE_ENERGY_BURNED -> + ActiveCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + HEART_RATE -> + HeartRateRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + samples = + listOf( + HeartRateRecord.Sample( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + ), + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + BODY_TEMPERATURE -> + BodyTemperatureRecord( + time = + Instant.ofEpochMilli( + startTime + ), + temperature = + Temperature.celsius( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + BODY_WATER_MASS -> + BodyWaterMassRecord( + time = + Instant.ofEpochMilli( + startTime + ), + mass = + Mass.kilograms( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + BLOOD_OXYGEN -> + OxygenSaturationRecord( + time = + Instant.ofEpochMilli( + startTime + ), + percentage = + Percentage( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + BLOOD_GLUCOSE -> + BloodGlucoseRecord( + time = + Instant.ofEpochMilli( + startTime + ), + level = + BloodGlucose.milligramsPerDeciliter( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + HEART_RATE_VARIABILITY_RMSSD -> + HeartRateVariabilityRmssdRecord( + time = + Instant.ofEpochMilli( + startTime + ), + heartRateVariabilityMillis = + value, + + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + DISTANCE_DELTA -> + DistanceRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + distance = + Length.meters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + WATER -> + HydrationRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + volume = + Volume.liters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_ASLEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_SLEEPING ) - result.success(false) - return - } - val access = permissions[i] - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_LIGHT -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_LIGHT ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_DEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_DEEP ) - } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } - } - scope.launch { - result.success( - healthConnectClient - .permissionController - .getGrantedPermissions() - .containsAll(permList), - ) - } - } - - private fun requestAuthorizationHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_REM -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_REM ) - result.success(false) - return - } - val access = permissions[i]!! - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_OUT_OF_BED -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_OUT_OF_BED ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_AWAKE -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_AWAKE ) - } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } - } - if (healthConnectRequestPermissionsLauncher == null) { - result.success(false) - Log.i("FLUTTER_HEALTH", "Permission launcher not found") - return - } - - healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) - } - - fun getHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - MapToHCType[dataType]?.let { classType -> - val records = mutableListOf() - - // Set up the initial request to read health records with specified - // parameters - var request = - ReadRecordsRequest( - recordType = classType, - // Define the maximum amount of data - // that HealthConnect can return - // in a single request - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) - - var response = healthConnectClient.readRecords(request) - var pageToken = response.pageToken - - // Add the records from the initial response to the records list - records.addAll(response.records) - - // Continue making requests and fetching records while there is a - // page token - while (!pageToken.isNullOrEmpty()) { - request = - ReadRecordsRequest( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - pageToken = pageToken - ) - response = healthConnectClient.readRecords(request) - - pageToken = response.pageToken - records.addAll(response.records) - } - - // Workout needs distance and total calories burned too - if (dataType == WORKOUT) { - for (rec in records) { - val record = rec as ExerciseSessionRecord - val distanceRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - DistanceRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalDistance = 0.0 - for (distanceRec in distanceRequest.records) { - totalDistance += - distanceRec.distance - .inMeters - } - - val energyBurnedRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - TotalCaloriesBurnedRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalEnergyBurned = 0.0 - for (energyBurnedRec in - energyBurnedRequest.records) { - totalEnergyBurned += - energyBurnedRec.energy - .inKilocalories - } - - val stepRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - StepsRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime - ), - ), - ) - var totalSteps = 0.0 - for (stepRec in stepRequest.records) { - totalSteps += stepRec.count - } - - // val metadata = (rec as Record).metadata - // Add final datapoint - healthConnectData.add( - // mapOf( - mapOf( - "workoutActivityType" to - (workoutTypeMapHealthConnect - .filterValues { - it == - record.exerciseType - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalSteps" to - if (totalSteps == - 0.0 - ) - null - else - totalSteps, - "totalStepsUnit" to - "COUNT", - "unit" to "MINUTES", - "date_from" to - rec.startTime - .toEpochMilli(), - "date_to" to - rec.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to - record.metadata - .dataOrigin - .packageName, - ), - ) - } - // Filter sleep stages for requested stage - } else if (classType == SleepSessionRecord::class) { - for (rec in response.records) { - if (rec is SleepSessionRecord) { - if (dataType == SLEEP_SESSION) { - healthConnectData.addAll( - convertRecord( - rec, - dataType - ) - ) - } else { - for (recStage in rec.stages) { - if (dataType == - MapSleepStageToType[ - recStage.stage] - ) { - healthConnectData - .addAll( - convertRecordStage( - recStage, - dataType, - rec.metadata.dataOrigin - .packageName - ) - ) - } - } - } - } - } - } else { - for (rec in records) { - healthConnectData.addAll( - convertRecord(rec, dataType) - ) - } - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } - } - - fun convertRecordStage( - stage: SleepSessionRecord.Stage, - dataType: String, - sourceName: String - ): List> { - return listOf( - mapOf( - "stage" to stage.stage, - "value" to - ChronoUnit.MINUTES.between( - stage.startTime, - stage.endTime - ), - "date_from" to stage.startTime.toEpochMilli(), - "date_to" to stage.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to sourceName, - ), + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + SLEEP_AWAKE_IN_BED -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_AWAKE_IN_BED + ) + ), + ) + + SLEEP_UNKNOWN -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_UNKNOWN + ) + ), + ) + SLEEP_SESSION -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + RESTING_HEART_RATE -> + RestingHeartRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + BASAL_ENERGY_BURNED -> + BasalMetabolicRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + basalMetabolicRate = + Power.kilocaloriesPerDay( + value + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + FLIGHTS_CLIMBED -> + FloorsClimbedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + floors = value, + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + RESPIRATORY_RATE -> + RespiratoryRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + rate = value, + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + // AGGREGATE_STEP_COUNT -> StepsRecord() + TOTAL_CALORIES_BURNED -> + TotalCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ) + + MENSTRUATION_FLOW -> MenstruationFlowRecord( + time = Instant.ofEpochMilli(startTime), + flow = value.toInt(), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) - } - fun getAggregateHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val interval = call.argument("interval")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - MapToHCAggregateMetric[dataType]?.let { metricClassType -> - val request = - AggregateGroupByDurationRequest( - metrics = setOf(metricClassType), - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - timeRangeSlicer = - Duration.ofSeconds( - interval - ) - ) - val response = healthConnectClient.aggregateGroupByDuration(request) - - for (durationResult in response) { - // The result may be null if no data is available in the - // time range - var totalValue = durationResult.result[metricClassType] - if (totalValue is Length) { - totalValue = totalValue.inMeters - } else if (totalValue is Energy) { - totalValue = totalValue.inKilocalories - } - - val packageNames = - durationResult.result.dataOrigins - .joinToString { origin -> - "${origin.packageName}" - } - - val data = - mapOf( - "value" to - (totalValue - ?: 0), - "date_from" to - durationResult.startTime - .toEpochMilli(), - "date_to" to - durationResult.endTime - .toEpochMilli(), - "source_name" to - packageNames, - "source_id" to "", - "is_manual_entry" to - packageNames.contains( - "user_input" - ) - ) - healthConnectData.add(data) - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } + BLOOD_PRESSURE_SYSTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + BLOOD_PRESSURE_DIASTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + WORKOUT -> + throw IllegalArgumentException( + "You must use the [writeWorkoutData] API " + ) + + NUTRITION -> + throw IllegalArgumentException( + "You must use the [writeMeal] API " + ) + + else -> + throw IllegalArgumentException( + "The type $type was not supported by the Health plugin or you must use another API " + ) + } + scope.launch { + try { + healthConnectClient.insertRecords(listOf(record)) + result.success(true) + } catch (e: Exception) { + result.success(false) + } } - - // TODO: Find alternative to SOURCE_ID or make it nullable? - fun convertRecord(record: Any, dataType: String): List> { - val metadata = (record as Record).metadata - when (record) { - is WeightRecord -> - return listOf( - mapOf( - "value" to - record.weight - .inKilograms, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HeightRecord -> - return listOf( - mapOf( - "value" to - record.height - .inMeters, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyFatRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is StepsRecord -> - return listOf( - mapOf( - "value" to record.count, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is ActiveCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HeartRateRecord -> - return record.samples.map { - mapOf( - "value" to it.beatsPerMinute, - "date_from" to - it.time.toEpochMilli(), - "date_to" to it.time.toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - } - is BodyTemperatureRecord -> - return listOf( - mapOf( - "value" to - record.temperature - .inCelsius, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyWaterMassRecord -> - return listOf( - mapOf( - "value" to - record.mass - .inKilograms, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BloodPressureRecord -> - return listOf( - mapOf( - "value" to - if (dataType == - BLOOD_PRESSURE_DIASTOLIC - ) - record.diastolic - .inMillimetersOfMercury - else - record.systolic - .inMillimetersOfMercury, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is OxygenSaturationRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BloodGlucoseRecord -> - return listOf( - mapOf( - "value" to - record.level - .inMilligramsPerDeciliter, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is DistanceRecord -> - return listOf( - mapOf( - "value" to - record.distance - .inMeters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HydrationRecord -> - return listOf( - mapOf( - "value" to - record.volume - .inLiters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is TotalCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is SleepSessionRecord -> - return listOf( - mapOf( - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "value" to - ChronoUnit.MINUTES - .between( - record.startTime, - record.endTime - ), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is RestingHeartRateRecord -> - return listOf( - mapOf( - "value" to - record.beatsPerMinute, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is FloorsClimbedRecord -> - return listOf( - mapOf( - "value" to record.floors, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is RespiratoryRateRecord -> - return listOf( - mapOf( - "value" to record.rate, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is NutritionRecord -> - return listOf( - mapOf( - "calories" to - record.energy!!.inKilocalories, - "protein" to - record.protein!!.inGrams, - "carbs" to - record.totalCarbohydrate!! - .inGrams, - "fat" to - record.totalFat!! - .inGrams, - "name" to record.name!!, - "mealType" to - (MapTypeToMealTypeHC[ - record.mealType] - ?: MEAL_TYPE_UNKNOWN), - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - // is ExerciseSessionRecord -> return listOf(mapOf("value" to , - // "date_from" to , - // "date_to" to , - // "source_id" to "", - // "source_name" to - // metadata.dataOrigin.packageName)) - else -> - throw IllegalArgumentException( - "Health data type not supported" - ) // TODO: Exception or error? - } + } + + /** Save a Workout session with options for distance and calories expended */ + private fun writeWorkoutData(call: MethodCall, result: Result) { + val type = call.argument("activityType")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val totalEnergyBurned = call.argument("totalEnergyBurned") + val totalDistance = call.argument("totalDistance") + val recordingMethod = call.argument("recordingMethod")!! + if (!workoutTypeMap.containsKey(type)) { + result.success(false) + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) + return } - - // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should - // not - // adopt a single type with attached stages approach - fun writeHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - val record = - when (type) { - BODY_FAT_PERCENTAGE -> - BodyFatRecord( - time = - Instant.ofEpochMilli( - startTime - ), - percentage = - Percentage( - value - ), - zoneOffset = null, - ) - HEIGHT -> - HeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - height = - Length.meters( - value - ), - zoneOffset = null, - ) - WEIGHT -> - WeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - weight = - Mass.kilograms( - value - ), - zoneOffset = null, - ) - STEPS -> - StepsRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - count = value.toLong(), - startZoneOffset = null, - endZoneOffset = null, - ) - ACTIVE_ENERGY_BURNED -> - ActiveCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - HEART_RATE -> - HeartRateRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - samples = - listOf< - HeartRateRecord.Sample>( - HeartRateRecord.Sample( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - ), - ), - startZoneOffset = null, - endZoneOffset = null, - ) - BODY_TEMPERATURE -> - BodyTemperatureRecord( - time = - Instant.ofEpochMilli( - startTime - ), - temperature = - Temperature.celsius( - value - ), - zoneOffset = null, - ) - BODY_WATER_MASS -> - BodyWaterMassRecord( - time = - Instant.ofEpochMilli( - startTime - ), - mass = - Mass.kilograms( - value - ), - zoneOffset = null, - ) - BLOOD_OXYGEN -> - OxygenSaturationRecord( - time = - Instant.ofEpochMilli( - startTime - ), - percentage = - Percentage( - value - ), - zoneOffset = null, - ) - BLOOD_GLUCOSE -> - BloodGlucoseRecord( - time = - Instant.ofEpochMilli( - startTime - ), - level = - BloodGlucose.milligramsPerDeciliter( - value - ), - zoneOffset = null, - ) - DISTANCE_DELTA -> - DistanceRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - distance = - Length.meters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - WATER -> - HydrationRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - volume = - Volume.liters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - SLEEP_ASLEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_SLEEPING - ) - ), - ) - SLEEP_LIGHT -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_LIGHT - ) - ), - ) - SLEEP_DEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_DEEP - ) - ), - ) - SLEEP_REM -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_REM - ) - ), - ) - SLEEP_OUT_OF_BED -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_OUT_OF_BED - ) - ), - ) - SLEEP_AWAKE -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_AWAKE - ) - ), - ) - SLEEP_SESSION -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - ) - RESTING_HEART_RATE -> - RestingHeartRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - zoneOffset = null, - ) - BASAL_ENERGY_BURNED -> - BasalMetabolicRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - basalMetabolicRate = - Power.kilocaloriesPerDay( - value - ), - zoneOffset = null, - ) - FLIGHTS_CLIMBED -> - FloorsClimbedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - floors = value, - startZoneOffset = null, - endZoneOffset = null, - ) - RESPIRATORY_RATE -> - RespiratoryRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - rate = value, - zoneOffset = null, - ) - // AGGREGATE_STEP_COUNT -> StepsRecord() - TOTAL_CALORIES_BURNED -> - TotalCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - BLOOD_PRESSURE_SYSTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - BLOOD_PRESSURE_DIASTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - WORKOUT -> - throw IllegalArgumentException( - "You must use the [writeWorkoutData] API " - ) - NUTRITION -> - throw IllegalArgumentException( - "You must use the [writeMeal] API " - ) - else -> - throw IllegalArgumentException( - "The type $type was not supported by the Health plugin or you must use another API " - ) - } - scope.launch { - try { - healthConnectClient.insertRecords(listOf(record)) - result.success(true) - } catch (e: Exception) { - result.success(false) - } + val workoutType = workoutTypeMap[type]!! + val title = call.argument("title") ?: type + + scope.launch { + try { + val list = mutableListOf() + list.add( + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + exerciseType = workoutType, + title = title, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ), + ) + if (totalDistance != null) { + list.add( + DistanceRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + distance = + Length.meters( + totalDistance.toDouble() + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ), + ) } + if (totalEnergyBurned != null) { + list.add( + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + energy = + Energy.kilocalories( + totalEnergyBurned + .toDouble() + ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ), + ) + } + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the workout", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - fun writeWorkoutHCData(call: MethodCall, result: Result) { - val type = call.argument("activityType")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - if (workoutTypeMapHealthConnect.containsKey(type) == false) { - result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) - return - } - val workoutType = workoutTypeMapHealthConnect[type]!! - val title = call.argument("title") ?: type - - scope.launch { - try { - val list = mutableListOf() - list.add( - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - exerciseType = workoutType, - title = title, - ), - ) - if (totalDistance != null) { - list.add( - DistanceRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - distance = - Length.meters( - totalDistance.toDouble() - ), - ), - ) - } - if (totalEnergyBurned != null) { - list.add( - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - energy = - Energy.kilocalories( - totalEnergyBurned - .toDouble() - ), - ), - ) - } - healthConnectClient.insertRecords( - list, - ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Workout was successfully added!" - ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the workout", - ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } + } + + /** Save a Blood Pressure measurement with systolic and diastolic values */ + private fun writeBloodPressure(call: MethodCall, result: Result) { + val systolic = call.argument("systolic")!! + val diastolic = call.argument("diastolic")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val recordingMethod = call.argument("recordingMethod")!! + + scope.launch { + try { + healthConnectClient.insertRecords( + listOf( + BloodPressureRecord( + time = startTime, + systolic = + Pressure.millimetersOfMercury( + systolic + ), + diastolic = + Pressure.millimetersOfMercury( + diastolic + ), + zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), + ), + ), + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Blood pressure was successfully added!", + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the blood pressure", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - fun writeBloodPressureHC(call: MethodCall, result: Result) { - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - - scope.launch { - try { - healthConnectClient.insertRecords( - listOf( - BloodPressureRecord( - time = startTime, - systolic = - Pressure.millimetersOfMercury( - systolic - ), - diastolic = - Pressure.millimetersOfMercury( - diastolic - ), - zoneOffset = null, - ), - ), - ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Blood pressure was successfully added!", - ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the blood pressure", - ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } + } + + /** Delete records of the given type in the time range */ + private fun deleteData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + if (!mapToType.containsKey(type)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $type not found in HC") + result.success(false) + return } - - fun deleteHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - if (!MapToHCType.containsKey(type)) { - Log.w("FLUTTER_HEALTH::ERROR", "Datatype " + type + " not found in HC") - result.success(false) - return - } - val classType = MapToHCType[type]!! - - scope.launch { - try { - healthConnectClient.deleteRecords( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) - result.success(true) - } catch (e: Exception) { - result.success(false) - } - } + val classType = mapToType[type]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) + result.success(true) + } catch (e: Exception) { + result.success(false) + } } - - val MapSleepStageToType = - hashMapOf( - 1 to SLEEP_AWAKE, - 2 to SLEEP_ASLEEP, - 3 to SLEEP_OUT_OF_BED, - 4 to SLEEP_LIGHT, - 5 to SLEEP_DEEP, - 6 to SLEEP_REM, - ) - - private val MapMealTypeToTypeHC = - hashMapOf( - BREAKFAST to MEAL_TYPE_BREAKFAST, - LUNCH to MEAL_TYPE_LUNCH, - DINNER to MEAL_TYPE_DINNER, - SNACK to MEAL_TYPE_SNACK, - MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, - ) - - private val MapTypeToMealTypeHC = - hashMapOf( - MEAL_TYPE_BREAKFAST to BREAKFAST, - MEAL_TYPE_LUNCH to LUNCH, - MEAL_TYPE_DINNER to DINNER, - MEAL_TYPE_SNACK to SNACK, - MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, - ) - - private val MapMealTypeToType = - hashMapOf( - BREAKFAST to Field.MEAL_TYPE_BREAKFAST, - LUNCH to Field.MEAL_TYPE_LUNCH, - DINNER to Field.MEAL_TYPE_DINNER, - SNACK to Field.MEAL_TYPE_SNACK, - MEAL_UNKNOWN to Field.MEAL_TYPE_UNKNOWN, - ) - - val MapToHCType = - hashMapOf( - BODY_FAT_PERCENTAGE to BodyFatRecord::class, - HEIGHT to HeightRecord::class, - WEIGHT to WeightRecord::class, - STEPS to StepsRecord::class, - AGGREGATE_STEP_COUNT to StepsRecord::class, - ACTIVE_ENERGY_BURNED to ActiveCaloriesBurnedRecord::class, - HEART_RATE to HeartRateRecord::class, - BODY_TEMPERATURE to BodyTemperatureRecord::class, - BODY_WATER_MASS to BodyWaterMassRecord::class, - BLOOD_PRESSURE_SYSTOLIC to BloodPressureRecord::class, - BLOOD_PRESSURE_DIASTOLIC to BloodPressureRecord::class, - BLOOD_OXYGEN to OxygenSaturationRecord::class, - BLOOD_GLUCOSE to BloodGlucoseRecord::class, - DISTANCE_DELTA to DistanceRecord::class, - WATER to HydrationRecord::class, - SLEEP_ASLEEP to SleepSessionRecord::class, - SLEEP_AWAKE to SleepSessionRecord::class, - SLEEP_LIGHT to SleepSessionRecord::class, - SLEEP_DEEP to SleepSessionRecord::class, - SLEEP_REM to SleepSessionRecord::class, - SLEEP_OUT_OF_BED to SleepSessionRecord::class, - SLEEP_SESSION to SleepSessionRecord::class, - WORKOUT to ExerciseSessionRecord::class, - NUTRITION to NutritionRecord::class, - RESTING_HEART_RATE to RestingHeartRateRecord::class, - BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, - FLIGHTS_CLIMBED to FloorsClimbedRecord::class, - RESPIRATORY_RATE to RespiratoryRateRecord::class, - TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class - // MOVE_MINUTES to TODO: Find alternative? - // TODO: Implement remaining types - // "ActiveCaloriesBurned" to - // ActiveCaloriesBurnedRecord::class, - // "BasalBodyTemperature" to - // BasalBodyTemperatureRecord::class, - // "BasalMetabolicRate" to BasalMetabolicRateRecord::class, - // "BloodGlucose" to BloodGlucoseRecord::class, - // "BloodPressure" to BloodPressureRecord::class, - // "BodyFat" to BodyFatRecord::class, - // "BodyTemperature" to BodyTemperatureRecord::class, - // "BoneMass" to BoneMassRecord::class, - // "CervicalMucus" to CervicalMucusRecord::class, - // "CyclingPedalingCadence" to - // CyclingPedalingCadenceRecord::class, - // "Distance" to DistanceRecord::class, - // "ElevationGained" to ElevationGainedRecord::class, - // "ExerciseSession" to ExerciseSessionRecord::class, - // "FloorsClimbed" to FloorsClimbedRecord::class, - // "HeartRate" to HeartRateRecord::class, - // "Height" to HeightRecord::class, - // "Hydration" to HydrationRecord::class, - // "LeanBodyMass" to LeanBodyMassRecord::class, - // "MenstruationFlow" to MenstruationFlowRecord::class, - // "MenstruationPeriod" to MenstruationPeriodRecord::class, - // "Nutrition" to NutritionRecord::class, - // "OvulationTest" to OvulationTestRecord::class, - // "OxygenSaturation" to OxygenSaturationRecord::class, - // "Power" to PowerRecord::class, - // "RespiratoryRate" to RespiratoryRateRecord::class, - // "RestingHeartRate" to RestingHeartRateRecord::class, - // "SexualActivity" to SexualActivityRecord::class, - // "SleepSession" to SleepSessionRecord::class, - // "SleepStage" to SleepStageRecord::class, - // "Speed" to SpeedRecord::class, - // "StepsCadence" to StepsCadenceRecord::class, - // "Steps" to StepsRecord::class, - // "TotalCaloriesBurned" to - // TotalCaloriesBurnedRecord::class, - // "Vo2Max" to Vo2MaxRecord::class, - // "Weight" to WeightRecord::class, - // "WheelchairPushes" to WheelchairPushesRecord::class, - ) - - val MapToHCAggregateMetric = - hashMapOf( - HEIGHT to HeightRecord.HEIGHT_AVG, - WEIGHT to WeightRecord.WEIGHT_AVG, - STEPS to StepsRecord.COUNT_TOTAL, - AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, - ACTIVE_ENERGY_BURNED to - ActiveCaloriesBurnedRecord - .ACTIVE_CALORIES_TOTAL, - HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, - DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, - WATER to HydrationRecord.VOLUME_TOTAL, - SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, - TOTAL_CALORIES_BURNED to - TotalCaloriesBurnedRecord.ENERGY_TOTAL - ) + } + + private val mapSleepStageToType = + hashMapOf( + 0 to SLEEP_UNKNOWN, + 1 to SLEEP_AWAKE, + 2 to SLEEP_ASLEEP, + 3 to SLEEP_OUT_OF_BED, + 4 to SLEEP_LIGHT, + 5 to SLEEP_DEEP, + 6 to SLEEP_REM, + 7 to SLEEP_AWAKE_IN_BED + ) + + private val mapMealTypeToType = + hashMapOf( + BREAKFAST to MEAL_TYPE_BREAKFAST, + LUNCH to MEAL_TYPE_LUNCH, + DINNER to MEAL_TYPE_DINNER, + SNACK to MEAL_TYPE_SNACK, + MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, + ) + + private val mapTypeToMealType = + hashMapOf( + MEAL_TYPE_BREAKFAST to BREAKFAST, + MEAL_TYPE_LUNCH to LUNCH, + MEAL_TYPE_DINNER to DINNER, + MEAL_TYPE_SNACK to SNACK, + MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, + ) + + + private val mapToType = + hashMapOf( + BODY_FAT_PERCENTAGE to BodyFatRecord::class, + LEAN_BODY_MASS to LeanBodyMassRecord::class, + HEIGHT to HeightRecord::class, + WEIGHT to WeightRecord::class, + STEPS to StepsRecord::class, + AGGREGATE_STEP_COUNT to StepsRecord::class, + ACTIVE_ENERGY_BURNED to ActiveCaloriesBurnedRecord::class, + HEART_RATE to HeartRateRecord::class, + BODY_TEMPERATURE to BodyTemperatureRecord::class, + BODY_WATER_MASS to BodyWaterMassRecord::class, + BLOOD_PRESSURE_SYSTOLIC to BloodPressureRecord::class, + BLOOD_PRESSURE_DIASTOLIC to BloodPressureRecord::class, + BLOOD_OXYGEN to OxygenSaturationRecord::class, + BLOOD_GLUCOSE to BloodGlucoseRecord::class, + HEART_RATE_VARIABILITY_RMSSD to HeartRateVariabilityRmssdRecord::class, + DISTANCE_DELTA to DistanceRecord::class, + WATER to HydrationRecord::class, + SLEEP_ASLEEP to SleepSessionRecord::class, + SLEEP_AWAKE to SleepSessionRecord::class, + SLEEP_AWAKE_IN_BED to SleepSessionRecord::class, + SLEEP_LIGHT to SleepSessionRecord::class, + SLEEP_DEEP to SleepSessionRecord::class, + SLEEP_REM to SleepSessionRecord::class, + SLEEP_OUT_OF_BED to SleepSessionRecord::class, + SLEEP_SESSION to SleepSessionRecord::class, + SLEEP_UNKNOWN to SleepSessionRecord::class, + WORKOUT to ExerciseSessionRecord::class, + NUTRITION to NutritionRecord::class, + RESTING_HEART_RATE to RestingHeartRateRecord::class, + BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, + FLIGHTS_CLIMBED to FloorsClimbedRecord::class, + RESPIRATORY_RATE to RespiratoryRateRecord::class, + TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class, + MENSTRUATION_FLOW to MenstruationFlowRecord::class, + // TODO: Implement remaining types + // "ActiveCaloriesBurned" to + // ActiveCaloriesBurnedRecord::class, + // "BasalBodyTemperature" to + // BasalBodyTemperatureRecord::class, + // "BasalMetabolicRate" to BasalMetabolicRateRecord::class, + // "BloodGlucose" to BloodGlucoseRecord::class, + // "BloodPressure" to BloodPressureRecord::class, + // "BodyTemperature" to BodyTemperatureRecord::class, + // "BoneMass" to BoneMassRecord::class, + // "CervicalMucus" to CervicalMucusRecord::class, + // "CyclingPedalingCadence" to + // CyclingPedalingCadenceRecord::class, + // "Distance" to DistanceRecord::class, + // "ElevationGained" to ElevationGainedRecord::class, + // "ExerciseSession" to ExerciseSessionRecord::class, + // "FloorsClimbed" to FloorsClimbedRecord::class, + // "HeartRate" to HeartRateRecord::class, + // "Height" to HeightRecord::class, + // "Hydration" to HydrationRecord::class, + // "MenstruationPeriod" to MenstruationPeriodRecord::class, + // "Nutrition" to NutritionRecord::class, + // "OvulationTest" to OvulationTestRecord::class, + // "OxygenSaturation" to OxygenSaturationRecord::class, + // "Power" to PowerRecord::class, + // "RespiratoryRate" to RespiratoryRateRecord::class, + // "RestingHeartRate" to RestingHeartRateRecord::class, + // "SexualActivity" to SexualActivityRecord::class, + // "SleepSession" to SleepSessionRecord::class, + // "SleepStage" to SleepStageRecord::class, + // "Speed" to SpeedRecord::class, + // "StepsCadence" to StepsCadenceRecord::class, + // "Steps" to StepsRecord::class, + // "TotalCaloriesBurned" to + // TotalCaloriesBurnedRecord::class, + // "Vo2Max" to Vo2MaxRecord::class, + // "Weight" to WeightRecord::class, + // "WheelchairPushes" to WheelchairPushesRecord::class, + ) + + private val mapToAggregateMetric = + hashMapOf( + HEIGHT to HeightRecord.HEIGHT_AVG, + WEIGHT to WeightRecord.WEIGHT_AVG, + STEPS to StepsRecord.COUNT_TOTAL, + AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, + ACTIVE_ENERGY_BURNED to + ActiveCaloriesBurnedRecord + .ACTIVE_CALORIES_TOTAL, + HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, + DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, + WATER to HydrationRecord.VOLUME_TOTAL, + SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, + TOTAL_CALORIES_BURNED to + TotalCaloriesBurnedRecord.ENERGY_TOTAL + ) + + private val workoutTypeMap = + mapOf( + // TODO: add skiing + // TODO: add skating + // TODO: add soccer + // TOOD: look into paddling + // TODO: add runnning + // TODO: look into hockey + "AMERICAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AMERICAN, + "AUSTRALIAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, + "BADMINTON" to + ExerciseSessionRecord + .EXERCISE_TYPE_BADMINTON, + "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, + "BASKETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_BASKETBALL, + "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, + // "BIKING_STATIONARY" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, + "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, + "CALISTHENICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_CALISTHENICS, + "CARDIO_DANCE" to + ExerciseSessionRecord + .EXERCISE_TYPE_DANCING, + "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, + "CROSS_COUNTRY_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, + "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, + "DOWNHILL_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, + "ELLIPTICAL" to + ExerciseSessionRecord + .EXERCISE_TYPE_ELLIPTICAL, + "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, + "FRISBEE_DISC" to + ExerciseSessionRecord + .EXERCISE_TYPE_FRISBEE_DISC, + "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, + "GUIDED_BREATHING" to + ExerciseSessionRecord + .EXERCISE_TYPE_GUIDED_BREATHING, + "GYMNASTICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_GYMNASTICS, + "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, + "HIGH_INTENSITY_INTERVAL_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, + "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, + // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, + "ICE_SKATING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ICE_SKATING, + "MARTIAL_ARTS" to + ExerciseSessionRecord + .EXERCISE_TYPE_MARTIAL_ARTS, + "PARAGLIDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_PARAGLIDING, + "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, + "RACQUETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RACQUETBALL, + "ROCK_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROCK_CLIMBING, + "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, + "ROWING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROWING_MACHINE, + "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, + "RUNNING_TREADMILL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RUNNING_TREADMILL, + "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, + "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, + "SCUBA_DIVING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SCUBA_DIVING, + "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, + "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, + "SNOWBOARDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWBOARDING, + "SNOWSHOEING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWSHOEING, + // "SOCCER" to ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, + "SOCIAL_DANCE" to + ExerciseSessionRecord + .EXERCISE_TYPE_DANCING, + "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, + "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, + "STAIR_CLIMBING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, + "STAIR_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING, + "STRENGTH_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STRENGTH_TRAINING, + "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, + "SWIMMING_OPEN_WATER" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_OPEN_WATER, + "SWIMMING_POOL" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_POOL, + "TABLE_TENNIS" to + ExerciseSessionRecord + .EXERCISE_TYPE_TABLE_TENNIS, + "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, + "VOLLEYBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_VOLLEYBALL, + "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, + "WATER_POLO" to + ExerciseSessionRecord + .EXERCISE_TYPE_WATER_POLO, + "WEIGHTLIFTING" to + ExerciseSessionRecord + .EXERCISE_TYPE_WEIGHTLIFTING, + "WHEELCHAIR" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + "WHEELCHAIR_RUN_PACE" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + "WHEELCHAIR_WALK_PACE" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, + "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT, + ) } diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle index 95636d301..63e07ba82 100644 --- a/packages/health/example/android/app/build.gradle +++ b/packages/health/example/android/app/build.gradle @@ -64,10 +64,6 @@ dependencies { implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' - implementation("com.google.android.gms:play-services-fitness:21.1.0") - implementation("com.google.android.gms:play-services-auth:20.2.0") androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - // The new health connect api - // implementation("androidx.health.connect:connect-client:1.0.0-alpha11") } diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml index d92e41cb1..996adaf97 100644 --- a/packages/health/example/android/app/src/main/AndroidManifest.xml +++ b/packages/health/example/android/app/src/main/AndroidManifest.xml @@ -60,6 +60,13 @@ + + + + + + + diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 4ce55d34f..77f2ffb80 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */, + 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -271,6 +272,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; AFF7CCF5217A091E1625CD54 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -293,6 +312,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + FEE6262278064D703A8D8D21 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -394,7 +431,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -531,7 +571,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -564,7 +607,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/health/example/ios/Runner/AppDelegate.swift b/packages/health/example/ios/Runner/AppDelegate.swift index 70693e4a8..b63630348 100644 --- a/packages/health/example/ios/Runner/AppDelegate.swift +++ b/packages/health/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 2e77ab376..59c963dac 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -7,11 +7,16 @@ import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; +// Global Health instance +final health = Health(); + void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { + const HealthApp({super.key}); + @override - _HealthAppState createState() => _HealthAppState(); + HealthAppState createState() => HealthAppState(); } enum AppState { @@ -27,12 +32,16 @@ enum AppState { DATA_NOT_DELETED, STEPS_READY, HEALTH_CONNECT_STATUS, + PERMISSIONS_REVOKING, + PERMISSIONS_REVOKED, + PERMISSIONS_NOT_REVOKED, } -class _HealthAppState extends State { +class HealthAppState extends State { List _healthDataList = []; AppState _state = AppState.DATA_NOT_FETCHED; int _nofSteps = 0; + List recordingMethodsToFilter = []; // All types available depending on platform (iOS ot Android). List get types => (Platform.isAndroid) @@ -55,25 +64,39 @@ class _HealthAppState extends State { // ]; // Set up corresponding permissions + // READ only - List get permissions => - types.map((e) => HealthDataAccess.READ).toList(); + // List get permissions => + // types.map((e) => HealthDataAccess.READ).toList(); // Or both READ and WRITE - // List get permissions => - // types.map((e) => HealthDataAccess.READ_WRITE).toList(); + List get permissions => types + .map((type) => + // can only request READ permissions to the following list of types on iOS + [ + HealthDataType.WALKING_HEART_RATE, + HealthDataType.ELECTROCARDIOGRAM, + HealthDataType.HIGH_HEART_RATE_EVENT, + HealthDataType.LOW_HEART_RATE_EVENT, + HealthDataType.IRREGULAR_HEART_RATE_EVENT, + HealthDataType.EXERCISE_TIME, + ].contains(type) + ? HealthDataAccess.READ + : HealthDataAccess.READ_WRITE) + .toList(); + @override void initState() { - // configure the health plugin before use. - Health().configure(useHealthConnectIfAvailable: true); + // configure the health plugin before use and check the Health Connect status + health.configure(); + health.getHealthConnectSdkStatus(); super.initState(); } /// Install Google Health Connect on this phone. - Future installHealthConnect() async { - await Health().installHealthConnect(); - } + Future installHealthConnect() async => + await health.installHealthConnect(); /// Authorize, i.e. get permissions to access relevant health data. Future authorize() async { @@ -87,7 +110,7 @@ class _HealthAppState extends State { // Check if we have health permissions bool? hasPermissions = - await Health().hasPermissions(types, permissions: permissions); + await health.hasPermissions(types, permissions: permissions); // hasPermissions = false because the hasPermission cannot disclose if WRITE access exists. // Hence, we have to request with WRITE as well. @@ -97,8 +120,8 @@ class _HealthAppState extends State { if (!hasPermissions) { // requesting access to the data types before reading them try { - authorized = await Health() - .requestAuthorization(types, permissions: permissions); + authorized = + await health.requestAuthorization(types, permissions: permissions); } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -112,10 +135,11 @@ class _HealthAppState extends State { Future getHealthConnectSdkStatus() async { assert(Platform.isAndroid, "This is only available on Android"); - final status = await Health().getHealthConnectSdkStatus(); + final status = await health.getHealthConnectSdkStatus(); setState(() { - _contentHealthConnectStatus = Text('Health Connect Status: $status'); + _contentHealthConnectStatus = + Text('Health Connect Status: ${status?.name.toUpperCase()}'); _state = AppState.HEALTH_CONNECT_STATUS; }); } @@ -126,22 +150,26 @@ class _HealthAppState extends State { // get data within the last 24 hours final now = DateTime.now(); - final yesterday = now.subtract(Duration(hours: 24)); + final yesterday = now.subtract(const Duration(hours: 24)); // Clear old data points _healthDataList.clear(); try { // fetch health data - List healthData = await Health().getHealthDataFromTypes( + List healthData = await health.getHealthDataFromTypes( types: types, startTime: yesterday, endTime: now, + recordingMethodsToFilter: recordingMethodsToFilter, ); debugPrint('Total number of data points: ${healthData.length}. ' '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); + // sort the data points by date + healthData.sort((a, b) => b.dateTo.compareTo(a.dateTo)); + // save all the new data points (only the first 100) _healthDataList.addAll( (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); @@ -150,9 +178,11 @@ class _HealthAppState extends State { } // filter out duplicates - _healthDataList = Health().removeDuplicates(_healthDataList); + _healthDataList = health.removeDuplicates(_healthDataList); - _healthDataList.forEach((data) => debugPrint(toJsonString(data))); + for (var data in _healthDataList) { + debugPrint(toJsonString(data)); + } // update the UI to display the results setState(() { @@ -165,117 +195,178 @@ class _HealthAppState extends State { /// following data types. Future addData() async { final now = DateTime.now(); - final earlier = now.subtract(Duration(minutes: 20)); + final earlier = now.subtract(const Duration(minutes: 20)); // Add data for supported types // NOTE: These are only the ones supported on Androids new API Health Connect. - // Both Android's Google Fit and iOS' HealthKit have more types that we support in the enum list [HealthDataType] + // Both Android's Health Connect and iOS' HealthKit have more types that we support in the enum list [HealthDataType] // Add more - like AUDIOGRAM, HEADACHE_SEVERE etc. to try them. bool success = true; // misc. health data examples using the writeHealthData() method - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.925, type: HealthDataType.HEIGHT, startTime: earlier, - endTime: now); - success &= await Health().writeHealthData( - value: 90, type: HealthDataType.WEIGHT, startTime: now); - success &= await Health().writeHealthData( + endTime: now, + recordingMethod: RecordingMethod.manual); + success &= await health.writeHealthData( + value: 90, + type: HealthDataType.WEIGHT, + startTime: now, + recordingMethod: RecordingMethod.manual); + success &= await health.writeHealthData( value: 90, type: HealthDataType.HEART_RATE, startTime: earlier, - endTime: now); - success &= await Health().writeHealthData( + endTime: now, + recordingMethod: RecordingMethod.manual); + success &= await health.writeHealthData( value: 90, type: HealthDataType.STEPS, startTime: earlier, - endTime: now); - success &= await Health().writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now); - success &= await Health().writeHealthData( + endTime: now, + recordingMethod: RecordingMethod.manual); + success &= await health.writeHealthData( + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + ); + success &= await health.writeHealthData( value: 70, type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + if (Platform.isIOS) { + success &= await health.writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, + startTime: earlier, + endTime: now); + } else { + success &= await health.writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now); + } + success &= await health.writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 105, type: HealthDataType.BLOOD_GLUCOSE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.8, type: HealthDataType.WATER, startTime: earlier, endTime: now); // different types of sleep - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_REM, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_ASLEEP, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_AWAKE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_DEEP, startTime: earlier, endTime: now); + success &= await health.writeHealthData( + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now, + ); // specialized write methods - success &= await Health().writeBloodOxygen( + success &= await health.writeBloodOxygen( saturation: 98, startTime: earlier, endTime: now, - flowRate: 1.0, ); - success &= await Health().writeWorkoutData( + success &= await health.writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", - start: now.subtract(Duration(minutes: 15)), + start: now.subtract(const Duration(minutes: 15)), end: now, totalDistance: 2430, totalEnergyBurned: 400, ); - success &= await Health().writeBloodPressure( + success &= await health.writeBloodPressure( systolic: 90, diastolic: 80, startTime: now, ); - success &= await Health().writeMeal( - mealType: MealType.SNACK, - startTime: earlier, - endTime: now, - caloriesConsumed: 1000, - carbohydrates: 50, - protein: 25, - fatTotal: 50, - name: "Banana", - caffeine: 0.002, - ); + success &= await health.writeMeal( + mealType: MealType.SNACK, + startTime: earlier, + endTime: now, + caloriesConsumed: 1000, + carbohydrates: 50, + protein: 25, + fatTotal: 50, + name: "Banana", + caffeine: 0.002, + vitaminA: 0.001, + vitaminC: 0.002, + vitaminD: 0.003, + vitaminE: 0.004, + vitaminK: 0.005, + b1Thiamin: 0.006, + b2Riboflavin: 0.007, + b3Niacin: 0.008, + b5PantothenicAcid: 0.009, + b6Pyridoxine: 0.010, + b7Biotin: 0.011, + b9Folate: 0.012, + b12Cobalamin: 0.013, + calcium: 0.015, + copper: 0.016, + iodine: 0.017, + iron: 0.018, + magnesium: 0.019, + manganese: 0.020, + phosphorus: 0.021, + potassium: 0.022, + selenium: 0.023, + sodium: 0.024, + zinc: 0.025, + water: 0.026, + molybdenum: 0.027, + chloride: 0.028, + chromium: 0.029, + cholesterol: 0.030, + fiber: 0.031, + fatMonounsaturated: 0.032, + fatPolyunsaturated: 0.033, + fatUnsaturated: 0.065, + fatTransMonoenoic: 0.65, + fatSaturated: 066, + sugar: 0.067, + recordingMethod: RecordingMethod.manual); // Store an Audiogram - only available on iOS // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; // const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0]; // const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5]; - // success &= await Health().writeAudiogram( + // success &= await health.writeAudiogram( // frequencies, // leftEarSensitivities, // rightEarSensitivities, @@ -287,6 +378,30 @@ class _HealthAppState extends State { // }, // ); + success &= await health.writeMenstruationFlow( + flow: MenstrualFlow.medium, + isStartOfCycle: true, + startTime: earlier, + endTime: now, + ); + + // Available on iOS 16.0+ only + if (Platform.isIOS) { + success &= await health.writeHealthData( + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); + + success &= await health.writeHealthData( + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual); + } + setState(() { _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; }); @@ -295,11 +410,11 @@ class _HealthAppState extends State { /// Delete some random health data. Future deleteData() async { final now = DateTime.now(); - final earlier = now.subtract(Duration(hours: 24)); + final earlier = now.subtract(const Duration(hours: 24)); bool success = true; for (HealthDataType type in types) { - success &= await Health().delete( + success &= await health.delete( type: type, startTime: earlier, endTime: now, @@ -320,15 +435,17 @@ class _HealthAppState extends State { final midnight = DateTime(now.year, now.month, now.day); bool stepsPermission = - await Health().hasPermissions([HealthDataType.STEPS]) ?? false; + await health.hasPermissions([HealthDataType.STEPS]) ?? false; if (!stepsPermission) { stepsPermission = - await Health().requestAuthorization([HealthDataType.STEPS]); + await health.requestAuthorization([HealthDataType.STEPS]); } if (stepsPermission) { try { - steps = await Health().getTotalStepsInInterval(midnight, now); + steps = await health.getTotalStepsInInterval(midnight, now, + includeManualEntry: + !recordingMethodsToFilter.contains(RecordingMethod.manual)); } catch (error) { debugPrint("Exception in getTotalStepsInInterval: $error"); } @@ -347,11 +464,22 @@ class _HealthAppState extends State { /// Revoke access to health data. Note, this only has an effect on Android. Future revokeAccess() async { + setState(() => _state = AppState.PERMISSIONS_REVOKING); + + bool success = false; + try { - await Health().revokePermissions(); + await health.revokePermissions(); + success = true; } catch (error) { debugPrint("Exception in revokeAccess: $error"); } + + setState(() { + _state = success + ? AppState.PERMISSIONS_REVOKED + : AppState.PERMISSIONS_NOT_REVOKED; + }); } // UI building below @@ -363,111 +491,218 @@ class _HealthAppState extends State { appBar: AppBar( title: const Text('Health Example'), ), - body: Container( - child: Column( - children: [ - Wrap( - spacing: 10, - children: [ + body: Column( + children: [ + Wrap( + spacing: 10, + children: [ + if (Platform.isAndroid) TextButton( - onPressed: authorize, - child: Text("Authenticate", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - if (Platform.isAndroid) + onPressed: getHealthConnectSdkStatus, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue)), + child: const Text("Check Health Connect Status", + style: TextStyle(color: Colors.white))), + if (Platform.isAndroid && + health.healthConnectSdkStatus != + HealthConnectSdkStatus.sdkAvailable) + TextButton( + onPressed: installHealthConnect, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue)), + child: const Text("Install Health Connect", + style: TextStyle(color: Colors.white))), + if (Platform.isIOS || + Platform.isAndroid && + health.healthConnectSdkStatus == + HealthConnectSdkStatus.sdkAvailable) + Wrap(spacing: 10, children: [ TextButton( - onPressed: getHealthConnectSdkStatus, - child: Text("Check Health Connect Status", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( + onPressed: authorize, + style: const ButtonStyle( backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: fetchData, - child: Text("Fetch Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: addData, - child: Text("Add Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: deleteData, - child: Text("Delete Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: fetchStepData, - child: Text("Fetch Step Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: revokeAccess, - child: Text("Revoke Access", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - if (Platform.isAndroid) + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Authenticate", + style: TextStyle(color: Colors.white))), TextButton( - onPressed: installHealthConnect, - child: Text("Install Health Connect", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( + onPressed: fetchData, + style: const ButtonStyle( backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - ], - ), - Divider(thickness: 3), - Expanded(child: Center(child: _content)) - ], - ), + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Fetch Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: addData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Add Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: deleteData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Delete Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: fetchStepData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Fetch Step Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: revokeAccess, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Revoke Access", + style: TextStyle(color: Colors.white))), + ]), + ], + ), + const Divider(thickness: 3), + if (_state == AppState.DATA_READY) _dataFiltration, + if (_state == AppState.STEPS_READY) _stepsFiltration, + Expanded(child: Center(child: _content)) + ], ), ), ); } + Widget get _dataFiltration => Column( + children: [ + Wrap( + children: [ + for (final method in Platform.isAndroid + ? [ + RecordingMethod.manual, + RecordingMethod.automatic, + RecordingMethod.active, + RecordingMethod.unknown, + ] + : [ + RecordingMethod.automatic, + RecordingMethod.manual, + ]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), + value: !recordingMethodsToFilter.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + recordingMethodsToFilter.remove(method); + } else { + recordingMethodsToFilter.add(method); + } + fetchData(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed + ], + ), + const Divider(thickness: 3), + ], + ); + + Widget get _stepsFiltration => Column( + children: [ + Wrap( + children: [ + for (final method in [ + RecordingMethod.manual, + ]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), + value: !recordingMethodsToFilter.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + recordingMethodsToFilter.remove(method); + } else { + recordingMethodsToFilter.add(method); + } + fetchStepData(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed + ], + ), + const Divider(thickness: 3), + ], + ); + + Widget get _permissionsRevoking => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator( + strokeWidth: 10, + )), + const Text('Revoking permissions...') + ], + ); + + Widget get _permissionsRevoked => const Text('Permissions revoked.'); + + Widget get _permissionsNotRevoked => + const Text('Failed to revoke permissions'); + Widget get _contentFetchingData => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - padding: EdgeInsets.all(20), - child: CircularProgressIndicator( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator( strokeWidth: 10, )), - Text('Fetching data...') + const Text('Fetching data...') ], ); Widget get _contentDataReady => ListView.builder( itemCount: _healthDataList.length, itemBuilder: (_, index) { + // filter out manual entires if not wanted + if (recordingMethodsToFilter + .contains(_healthDataList[index].recordingMethod)) { + return Container(); + } + 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}'), + trailing: Text(p.unitString), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } 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}'), + trailing: + Text((p.value as WorkoutHealthValue).workoutActivityType.name), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } if (p.value is NutritionHealthValue) { @@ -476,52 +711,51 @@ class _HealthAppState extends State { "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), trailing: Text('${(p.value as NutritionHealthValue).calories} kcal'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } return ListTile( title: Text("${p.typeString}: ${p.value}"), - trailing: Text('${p.unitString}'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + trailing: Text(p.unitString), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); }); - Widget _contentNoData = const Text('No Data to show'); + final Widget _contentNoData = const Text('No Data to show'); - Widget _contentNotFetched = const Column(children: [ - const Text("Press 'Auth' to get permissions to access health data."), - const Text("Press 'Fetch Dat' to get health data."), - const Text("Press 'Add Data' to add some random health data."), - const Text("Press 'Delete Data' to remove some random health data."), - ], mainAxisAlignment: MainAxisAlignment.center); + final Widget _contentNotFetched = + const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text("Press 'Auth' to get permissions to access health data."), + Text("Press 'Fetch Dat' to get health data."), + Text("Press 'Add Data' to add some random health data."), + Text("Press 'Delete Data' to remove some random health data."), + ]); - Widget _authorized = const Text('Authorization granted!'); + final Widget _authorized = const Text('Authorization granted!'); - Widget _authorizationNotGranted = const Column( + final Widget _authorizationNotGranted = const Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Authorization not given.'), - const Text( - 'For Google Fit please check your OAUTH2 client ID is correct in Google Developer Console.'), - const Text( + Text('Authorization not given.'), + Text( 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.'), - const Text('For Apple Health check your permissions in Apple Health.'), + Text('For Apple Health check your permissions in Apple Health.'), ], - mainAxisAlignment: MainAxisAlignment.center, ); Widget _contentHealthConnectStatus = const Text( 'No status, click getHealthConnectSdkStatus to get the status.'); - Widget _dataAdded = const Text('Data points inserted successfully.'); + final Widget _dataAdded = const Text('Data points inserted successfully.'); - Widget _dataDeleted = const Text('Data points deleted successfully.'); + final Widget _dataDeleted = const Text('Data points deleted successfully.'); Widget get _stepsFetched => Text('Total number of steps: $_nofSteps.'); - Widget _dataNotAdded = + final Widget _dataNotAdded = const Text('Failed to add data.\nDo you have permissions to add data?'); - Widget _dataNotDeleted = const Text('Failed to delete data'); + final Widget _dataNotDeleted = const Text('Failed to delete data'); Widget get _content => switch (_state) { AppState.DATA_READY => _contentDataReady, @@ -536,5 +770,8 @@ class _HealthAppState extends State { AppState.DATA_NOT_DELETED => _dataNotDeleted, AppState.STEPS_READY => _stepsFetched, AppState.HEALTH_CONNECT_STATUS => _contentHealthConnectStatus, + AppState.PERMISSIONS_REVOKING => _permissionsRevoking, + AppState.PERMISSIONS_REVOKED => _permissionsRevoked, + AppState.PERMISSIONS_NOT_REVOKED => _permissionsNotRevoked, }; } diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index 6bfe0c493..5c6f70be8 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -33,6 +33,7 @@ const List dataTypesIOS = [ HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_LIGHT, HealthDataType.SLEEP_DEEP, HealthDataType.SLEEP_REM, HealthDataType.WATER, @@ -43,22 +44,30 @@ const List dataTypesIOS = [ HealthDataType.HEADACHE_MODERATE, HealthDataType.HEADACHE_SEVERE, HealthDataType.HEADACHE_UNSPECIFIED, + HealthDataType.LEAN_BODY_MASS, // note that a phone cannot write these ECG-based types - only read them - HealthDataType.ELECTROCARDIOGRAM, - HealthDataType.HIGH_HEART_RATE_EVENT, - HealthDataType.IRREGULAR_HEART_RATE_EVENT, - HealthDataType.LOW_HEART_RATE_EVENT, - HealthDataType.RESTING_HEART_RATE, - HealthDataType.WALKING_HEART_RATE, + // HealthDataType.ELECTROCARDIOGRAM, + // HealthDataType.HIGH_HEART_RATE_EVENT, + // HealthDataType.IRREGULAR_HEART_RATE_EVENT, + // HealthDataType.LOW_HEART_RATE_EVENT, + // HealthDataType.RESTING_HEART_RATE, + // HealthDataType.WALKING_HEART_RATE, + // HealthDataType.ATRIAL_FIBRILLATION_BURDEN, HealthDataType.NUTRITION, + HealthDataType.GENDER, + HealthDataType.BLOOD_TYPE, + HealthDataType.BIRTH_DATE, + HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, ]; /// List of data types available on Android. /// /// Note that these are only the ones supported on Android's Health Connect API. -/// Android's Google Fit have more types that we support in the [HealthDataType] +/// Android's Health Connect has more types that we support in the [HealthDataType] /// enumeration. const List dataTypesAndroid = [ HealthDataType.ACTIVE_ENERGY_BURNED, @@ -70,18 +79,22 @@ const List dataTypesAndroid = [ HealthDataType.BODY_FAT_PERCENTAGE, HealthDataType.HEIGHT, HealthDataType.WEIGHT, + HealthDataType.LEAN_BODY_MASS, // HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.HEART_RATE, + HealthDataType.HEART_RATE_VARIABILITY_RMSSD, HealthDataType.STEPS, - // HealthDataType.MOVE_MINUTES, // TODO: Find alternative for Health Connect HealthDataType.DISTANCE_DELTA, HealthDataType.RESPIRATORY_RATE, - HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, - HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_AWAKE_IN_BED, + HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_OUT_OF_BED, HealthDataType.SLEEP_REM, + HealthDataType.SLEEP_UNKNOWN, HealthDataType.SLEEP_SESSION, HealthDataType.WATER, HealthDataType.WORKOUT, @@ -89,4 +102,5 @@ const List dataTypesAndroid = [ HealthDataType.FLIGHTS_CLIMBED, HealthDataType.NUTRITION, HealthDataType.TOTAL_CALORIES_BURNED, + HealthDataType.MENSTRUATION_FLOW, ]; diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index f23767d27..031bcc6c2 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -11,8 +11,8 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 - permission_handler: ^10.2.0 - carp_serializable: ^1.1.0 # polymorphic json serialization + permission_handler: ^11.3.1 + carp_serializable: ^2.0.0 # polymorphic json serialization health: path: ../ diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 7d5b3eb64..ed51986aa 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -2,11 +2,19 @@ import Flutter import HealthKit import UIKit -public class SwiftHealthPlugin: NSObject, FlutterPlugin { +enum RecordingMethod: Int { + case unknown = 0 // RECORDING_METHOD_UNKNOWN (not supported on iOS) + case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED (not supported on iOS) + case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED + case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY +} +public class SwiftHealthPlugin: NSObject, FlutterPlugin { + let healthStore = HKHealthStore() var healthDataTypes = [HKSampleType]() var healthDataQuantityTypes = [HKQuantityType]() + var characteristicsDataTypes = [HKCharacteristicType]() var heartRateEventTypes = Set() var headacheType = Set() var allDataTypes = Set() @@ -14,9 +22,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { var dataQuantityTypesDict: [String: HKQuantityType] = [:] var unitDict: [String: HKUnit] = [:] var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] - + var characteristicsTypesDict: [String: HKCharacteristicType] = [:] + var nutritionList: [String] = [] + // Health Data Type Keys let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" + let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN" let AUDIOGRAM = "AUDIOGRAM" let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" @@ -24,18 +35,98 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" + let LEAN_BODY_MASS = "LEAN_BODY_MASS" let BODY_MASS_INDEX = "BODY_MASS_INDEX" let BODY_TEMPERATURE = "BODY_TEMPERATURE" + // Nutrition let DIETARY_CARBS_CONSUMED = "DIETARY_CARBS_CONSUMED" let DIETARY_ENERGY_CONSUMED = "DIETARY_ENERGY_CONSUMED" let DIETARY_FATS_CONSUMED = "DIETARY_FATS_CONSUMED" let DIETARY_PROTEIN_CONSUMED = "DIETARY_PROTEIN_CONSUMED" let DIETARY_CAFFEINE = "DIETARY_CAFFEINE" + let DIETARY_FIBER = "DIETARY_FIBER" + let DIETARY_SUGAR = "DIETARY_SUGAR" + let DIETARY_FAT_MONOUNSATURATED = "DIETARY_FAT_MONOUNSATURATED" + let DIETARY_FAT_POLYUNSATURATED = "DIETARY_FAT_POLYUNSATURATED" + let DIETARY_FAT_SATURATED = "DIETARY_FAT_SATURATED" + let DIETARY_CHOLESTEROL = "DIETARY_CHOLESTEROL" + let DIETARY_VITAMIN_A = "DIETARY_VITAMIN_A" + let DIETARY_THIAMIN = "DIETARY_THIAMIN" + let DIETARY_RIBOFLAVIN = "DIETARY_RIBOFLAVIN" + let DIETARY_NIACIN = "DIETARY_NIACIN" + let DIETARY_PANTOTHENIC_ACID = "DIETARY_PANTOTHENIC_ACID" + let DIETARY_VITAMIN_B6 = "DIETARY_VITAMIN_B6" + let DIETARY_BIOTIN = "DIETARY_BIOTIN" + let DIETARY_VITAMIN_B12 = "DIETARY_VITAMIN_B12" + let DIETARY_VITAMIN_C = "DIETARY_VITAMIN_C" + let DIETARY_VITAMIN_D = "DIETARY_VITAMIN_D" + let DIETARY_VITAMIN_E = "DIETARY_VITAMIN_E" + let DIETARY_VITAMIN_K = "DIETARY_VITAMIN_K" + let DIETARY_FOLATE = "DIETARY_FOLATE" + let DIETARY_CALCIUM = "DIETARY_CALCIUM" + let DIETARY_CHLORIDE = "DIETARY_CHLORIDE" + let DIETARY_IRON = "DIETARY_IRON" + let DIETARY_MAGNESIUM = "DIETARY_MAGNESIUM" + let DIETARY_PHOSPHORUS = "DIETARY_PHOSPHORUS" + let DIETARY_POTASSIUM = "DIETARY_POTASSIUM" + let DIETARY_SODIUM = "DIETARY_SODIUM" + let DIETARY_ZINC = "DIETARY_ZINC" + let DIETARY_WATER = "WATER" + let DIETARY_CHROMIUM = "DIETARY_CHROMIUM" + let DIETARY_COPPER = "DIETARY_COPPER" + let DIETARY_IODINE = "DIETARY_IODINE" + let DIETARY_MANGANESE = "DIETARY_MANGANESE" + let DIETARY_MOLYBDENUM = "DIETARY_MOLYBDENUM" + let DIETARY_SELENIUM = "DIETARY_SELENIUM" + let NUTRITION_KEYS: [String: HKQuantityTypeIdentifier] = [ + "calories": .dietaryEnergyConsumed, + "protein": .dietaryProtein, + "carbs": .dietaryCarbohydrates, + "fat": .dietaryFatTotal, + "caffeine": .dietaryCaffeine, + "vitamin_a": .dietaryVitaminA, + "b1_thiamine": .dietaryThiamin, + "b2_riboflavin": .dietaryRiboflavin, + "b3_niacin" : .dietaryNiacin, + "b5_pantothenic_acid" : .dietaryPantothenicAcid, + "b6_pyridoxine" : .dietaryVitaminB6, + "b7_biotin" : .dietaryBiotin, + "b9_folate" : .dietaryFolate, + "b12_cobalamin": .dietaryVitaminB12, + "vitamin_c": .dietaryVitaminC, + "vitamin_d": .dietaryVitaminD, + "vitamin_e": .dietaryVitaminE, + "vitamin_k": .dietaryVitaminK, + "calcium": .dietaryCalcium, + "chloride": .dietaryChloride, + "cholesterol": .dietaryCholesterol, + "chromium": .dietaryChromium, + "copper": .dietaryCopper, + "fat_unsaturated": .dietaryFatMonounsaturated, + "fat_monounsaturated": .dietaryFatMonounsaturated, + "fat_polyunsaturated": .dietaryFatPolyunsaturated, + "fat_saturated": .dietaryFatSaturated, + // "fat_trans_monoenoic": .dietaryFatTransMonoenoic, + "fiber": .dietaryFiber, + "iodine": .dietaryIodine, + "iron": .dietaryIron, + "magnesium": .dietaryMagnesium, + "manganese": .dietaryManganese, + "molybdenum": .dietaryMolybdenum, + "phosphorus": .dietaryPhosphorus, + "potassium": .dietaryPotassium, + "selenium": .dietarySelenium, + "sodium": .dietarySodium, + "sugar": .dietarySugar, + "water": .dietaryWater, + "zinc": .dietaryZinc, + ] let ELECTRODERMAL_ACTIVITY = "ELECTRODERMAL_ACTIVITY" let FORCED_EXPIRATORY_VOLUME = "FORCED_EXPIRATORY_VOLUME" let HEART_RATE = "HEART_RATE" let HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN" let HEIGHT = "HEIGHT" + let INSULIN_DELIVERY = "INSULIN_DELIVERY" let HIGH_HEART_RATE_EVENT = "HIGH_HEART_RATE_EVENT" let IRREGULAR_HEART_RATE_EVENT = "IRREGULAR_HEART_RATE_EVENT" let LOW_HEART_RATE_EVENT = "LOW_HEART_RATE_EVENT" @@ -50,18 +141,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let DISTANCE_SWIMMING = "DISTANCE_SWIMMING" let DISTANCE_CYCLING = "DISTANCE_CYCLING" let FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" - let WATER = "WATER" let MINDFULNESS = "MINDFULNESS" - let SLEEP_IN_BED = "SLEEP_IN_BED" let SLEEP_ASLEEP = "SLEEP_ASLEEP" - let SLEEP_ASLEEP_CORE = "SLEEP_ASLEEP_CORE" - let SLEEP_ASLEEP_DEEP = "SLEEP_ASLEEP_DEEP" - let SLEEP_ASLEEP_REM = "SLEEP_ASLEEP_REM" let SLEEP_AWAKE = "SLEEP_AWAKE" let SLEEP_DEEP = "SLEEP_DEEP" + let SLEEP_IN_BED = "SLEEP_IN_BED" + let SLEEP_LIGHT = "SLEEP_LIGHT" let SLEEP_REM = "SLEEP_REM" let TIME_IN_DAYLIGHT = "TIME_IN_DAYLIGHT" - let EXERCISE_TIME = "EXERCISE_TIME" let WORKOUT = "WORKOUT" let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" @@ -71,7 +158,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let HEADACHE_SEVERE = "HEADACHE_SEVERE" let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" let NUTRITION = "NUTRITION" - + let BIRTH_DATE = "BIRTH_DATE" + let GENDER = "GENDER" + let BLOOD_TYPE = "BLOOD_TYPE" + let MENSTRUATION_FLOW = "MENSTRUATION_FLOW" + let WATER_TEMPERATURE = "WATER_TEMPERATURE" + let UNDERWATER_DEPTH = "UNDERWATER_DEPTH" + + // Health Unit types // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet @@ -123,22 +217,22 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" let UNKNOWN_UNIT = "UNKNOWN_UNIT" let NO_UNIT = "NO_UNIT" - + struct PluginError: Error { let message: String } - + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "flutter_health", binaryMessenger: registrar.messenger()) let instance = SwiftHealthPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } - + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // Set up all data types initializeTypes() - + /// Handle checkIfHealthDataAvailable if call.method.elementsEqual("checkIfHealthDataAvailable") { checkIfHealthDataAvailable(call: call, result: result) @@ -146,69 +240,72 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("requestAuthorization") { try! requestAuthorization(call: call, result: result) } - + /// Handle getData else if call.method.elementsEqual("getData") { getData(call: call, result: result) } - + /// Handle getIntervalData else if (call.method.elementsEqual("getIntervalData")){ getIntervalData(call: call, result: result) } - + /// Handle getTotalStepsInInterval else if call.method.elementsEqual("getTotalStepsInInterval") { getTotalStepsInInterval(call: call, result: result) } - + /// Handle writeData else if call.method.elementsEqual("writeData") { try! writeData(call: call, result: result) } - + /// Handle writeAudiogram else if call.method.elementsEqual("writeAudiogram") { try! writeAudiogram(call: call, result: result) } - + /// Handle writeBloodPressure else if call.method.elementsEqual("writeBloodPressure") { try! writeBloodPressure(call: call, result: result) } - + /// Handle writeMeal else if (call.method.elementsEqual("writeMeal")){ try! writeMeal(call: call, result: result) } - + + /// Handle writeInsulinDelivery + else if (call.method.elementsEqual("writeInsulinDelivery")){ + try! writeInsulinDelivery(call: call, result: result) + } + /// Handle writeWorkoutData else if call.method.elementsEqual("writeWorkoutData") { try! writeWorkoutData(call: call, result: result) } - + + /// Handle writeMenstruationFlow + else if call.method.elementsEqual("writeMenstruationFlow") { + try! writeMenstruationFlow(call: call, result: result) + } + /// Handle hasPermission else if call.method.elementsEqual("hasPermissions") { try! hasPermissions(call: call, result: result) } - + /// Handle delete data else if call.method.elementsEqual("delete") { try! delete(call: call, result: result) } - - /// Disconnect - else if (call.method.elementsEqual("disconnect")){ - // Do nothing. - result(true) - } - } - + func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { result(HKHealthStore.isHealthDataAvailable()) } - + func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { let arguments = call.arguments as? NSDictionary guard var types = arguments?["types"] as? [String], @@ -217,24 +314,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - + if let nutritionIndex = types.firstIndex(of: NUTRITION) { types.remove(at: nutritionIndex) let nutritionPermission = permissions[nutritionIndex] permissions.remove(at: nutritionIndex) - - types.append(DIETARY_ENERGY_CONSUMED) - permissions.append(nutritionPermission) - types.append(DIETARY_CARBS_CONSUMED) - permissions.append(nutritionPermission) - types.append(DIETARY_PROTEIN_CONSUMED) - permissions.append(nutritionPermission) - types.append(DIETARY_FATS_CONSUMED) - permissions.append(nutritionPermission) - types.append(DIETARY_CAFFEINE) - permissions.append(nutritionPermission) + + for nutritionType in nutritionList { + types.append(nutritionType) + permissions.append(nutritionPermission) + } } - + for (index, type) in types.enumerated() { let sampleType = dataTypeLookUp(key: type) let success = hasPermission(type: sampleType, access: permissions[index]) @@ -242,13 +333,20 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(success) return } + if let characteristicType = characteristicsTypesDict[type] { + let characteristicSuccess = hasPermission(type: characteristicType, access: permissions[index]) + if (characteristicSuccess == nil || characteristicSuccess == false) { + result(characteristicSuccess) + return + } + } } - + result(true) } - - func hasPermission(type: HKSampleType, access: Int) -> Bool? { - + + func hasPermission(type: HKObjectType, access: Int) -> Bool? { + if #available(iOS 13.0, *) { let status = healthStore.authorizationStatus(for: type) switch access { @@ -263,7 +361,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { return nil } } - + func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let types = arguments["types"] as? [String], @@ -272,21 +370,15 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments!") } - - var typesToRead = Set() + + var typesToRead = Set() var typesToWrite = Set() for (index, key) in types.enumerated() { if (key == NUTRITION) { - let caloriesType = dataTypeLookUp(key: DIETARY_ENERGY_CONSUMED) - let carbsType = dataTypeLookUp(key: DIETARY_CARBS_CONSUMED) - let proteinType = dataTypeLookUp(key: DIETARY_PROTEIN_CONSUMED) - let fatType = dataTypeLookUp(key: DIETARY_FATS_CONSUMED) - let caffeineType = dataTypeLookUp(key: DIETARY_CAFFEINE) - typesToWrite.insert(caloriesType); - typesToWrite.insert(carbsType); - typesToWrite.insert(proteinType); - typesToWrite.insert(fatType); - typesToWrite.insert(caffeineType); + for nutritionType in nutritionList { + let nutritionData = dataTypeLookUp(key: nutritionType) + typesToWrite.insert(nutritionData) + } } else { let dataType = dataTypeLookUp(key: key) let access = permissions[index] @@ -299,9 +391,20 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { typesToRead.insert(dataType) typesToWrite.insert(dataType) } + if let characteristicsType = characteristicsTypesDict[key] { + let access = permissions[index] + switch access { + case 0: + typesToRead.insert(characteristicsType) + case 1: + throw PluginError(message: "Can not ask for reading permissions to the type of \(characteristicsType)") + default: + break + } + } } } - + if #available(iOS 13.0, *) { healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in @@ -313,34 +416,40 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(false) // Handle the error here. } } - + func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let value = (arguments["value"] as? Double), let type = (arguments["dataTypeKey"] as? String), let unit = (arguments["dataUnitKey"] as? String), let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) + let endTime = (arguments["endTime"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + let metadata: [String: Any] = [ + HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) + ] + let sample: HKObject - + if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { sample = HKCategorySample( type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, - end: dateTo) + end: dateTo, metadata: metadata) } else { let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) sample = HKQuantitySample( type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, - end: dateTo) + end: dateTo, metadata: metadata) } - + HKHealthStore().save( sample, withCompletion: { (success, error) in @@ -352,7 +461,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let frequencies = (arguments["frequencies"] as? [Double]), @@ -363,12 +472,12 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments") } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var sensitivityPoints = [HKAudiogramSensitivityPoint]() - + for index in 0...frequencies.count - 1 { let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) let dbUnit = HKUnit.decibelHearingLevel() @@ -378,23 +487,23 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) sensitivityPoints.append(sensitivityPoint) } - + let audiogram: HKAudiogramSample let metadataReceived = (arguments["metadata"] as? [String: Any]?) - + if (metadataReceived) != nil { guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return } guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return } - + audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID]) - + } else { audiogram = HKAudiogramSample( sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil) } - + HKHealthStore().save( audiogram, withCompletion: { (success, error) in @@ -406,31 +515,37 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let systolic = (arguments["systolic"] as? Double), let diastolic = (arguments["diastolic"] as? Double), let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) + let endTime = (arguments["endTime"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) else { throw PluginError(message: "Invalid Arguments") } let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + let metadata = [ + HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) + ] + let systolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), - start: dateFrom, end: dateTo) + start: dateFrom, end: dateTo, metadata: metadata) let diastolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), - start: dateFrom, end: dateTo) + start: dateFrom, end: dateTo, metadata: metadata) let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)! let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample) let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation) - + HKHealthStore().save( [blood_pressure_sample], withCompletion: { (success, error) in @@ -442,61 +557,44 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber), - let calories = (arguments["caloriesConsumed"] as? Double?) ?? 0, - let carbs = (arguments["carbohydrates"] as? Double?) ?? 0, - let protein = (arguments["protein"] as? Double?) ?? 0, - let fat = (arguments["fatTotal"] as? Double?) ?? 0, let name = (arguments["name"] as? String?), - let caffeine = (arguments["caffeine"] as? Double?) ?? 0, - let mealType = (arguments["mealType"] as? String?) + let startTime = (arguments["start_time"] as? NSNumber), + let endTime = (arguments["end_time"] as? NSNumber), + let mealType = (arguments["meal_type"] as? String?), + let recordingMethod = arguments["recordingMethod"] as? Int else { throw PluginError(message: "Invalid Arguments") } + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let mealTypeString = mealType ?? "UNKNOWN" - var mealTypeString = mealType ?? "UNKNOWN" - var metadata = ["HKFoodMeal": "\(mealTypeString)"] - - if(name != nil) { + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + + var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] + if (name != nil) { metadata[HKMetadataKeyFoodType] = "\(name!)" } var nutrition = Set() - - if(calories > 0) { - let caloriesSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!, quantity: HKQuantity(unit: HKUnit.kilocalorie(), doubleValue: calories), start: dateFrom, end: dateTo, metadata: metadata) - nutrition.insert(caloriesSample) - } - - if(carbs > 0) { - let carbsSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: carbs), start: dateFrom, end: dateTo, metadata: metadata) - nutrition.insert(carbsSample) - } - - if(protein > 0) { - let proteinSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryProtein)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: protein), start: dateFrom, end: dateTo, metadata: metadata) - nutrition.insert(proteinSample) - } - - if(fat > 0) { - let fatSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: fat), start: dateFrom, end: dateTo, metadata: metadata) - nutrition.insert(fatSample) - } - - if(caffeine > 0) { - let caffeineSample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)!, quantity: HKQuantity(unit: HKUnit.gram(), doubleValue: caffeine), start: dateFrom, end: dateTo, metadata: metadata) - nutrition.insert(caffeineSample) + for (key, identifier) in NUTRITION_KEYS { + let value = arguments[key] as? Double + guard let unwrappedValue = value else { continue } + let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + let nutritionSample = HKQuantitySample( + type: HKSampleType.quantityType(forIdentifier: identifier)!, quantity: HKQuantity(unit: unit, doubleValue: unwrappedValue), start: dateFrom, end: dateTo, metadata: metadata) + nutrition.insert(nutritionSample) } if #available(iOS 15.0, *){ - let meal = HKCorrelation.init(type: HKCorrelationType.init(HKCorrelationTypeIdentifier.food), start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata) - + let type = HKCorrelationType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.food)! + let meal = HKCorrelation(type: type, start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata) + HKHealthStore().save(meal, withCompletion: { (success, error) in if let err = error { print("Error Saving Meal Sample: \(err.localizedDescription)") @@ -509,7 +607,77 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(false) } } + func writeInsulinDelivery(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let units = (arguments["units"] as? Double), + let reason = (arguments["reason"] as? NSNumber), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber) + else { + throw PluginError(message: "Invalid Arguments") + } + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let type = HKSampleType.quantityType(forIdentifier: .insulinDelivery)! + let quantity = HKQuantity(unit: HKUnit.internationalUnit(), doubleValue: units) + let metadata = [HKMetadataKeyInsulinDeliveryReason: reason] + + let insulin_sample = HKQuantitySample(type: type, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) + + HKHealthStore().save(insulin_sample, withCompletion: { (success, error) in + if let err = error { + print("Error Saving Insulin Delivery Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } + + func writeMenstruationFlow(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let flow = (arguments["value"] as? Int), + let endTime = (arguments["endTime"] as? NSNumber), + let isStartOfCycle = (arguments["isStartOfCycle"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) + else { + throw PluginError(message: "Invalid Arguments - value, startTime, endTime or isStartOfCycle invalid") + } + guard let menstrualFlowType = HKCategoryValueMenstrualFlow(rawValue: flow) else { + throw PluginError(message: "Invalid Menstrual Flow Type") + } + + let dateTime = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + + guard let categoryType = HKSampleType.categoryType(forIdentifier: .menstrualFlow) else { + throw PluginError(message: "Invalid Menstrual Flow Type") + } + let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] + + let sample = HKCategorySample( + type: categoryType, + value: menstrualFlowType.rawValue, + start: dateTime, + end: dateTime, + metadata: metadata + ) + + HKHealthStore().save( + sample, + withCompletion: { (success, error) in + if let err = error { + print("Error Saving Menstruation Flow Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } + func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, let activityType = (arguments["activityType"] as? String), @@ -519,10 +687,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else { throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") } - + var totalEnergyBurned: HKQuantity? var totalDistance: HKQuantity? = nil - + // Handle optional arguments if let teb = (arguments["totalEnergyBurned"] as? Double) { totalEnergyBurned = HKQuantity( @@ -532,17 +700,17 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { totalDistance = HKQuantity( unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) } - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + var workout: HKWorkout - + workout = HKWorkout( activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom), totalEnergyBurned: totalEnergyBurned ?? nil, totalDistance: totalDistance ?? nil, metadata: nil) - + HKHealthStore().save( workout, withCompletion: { (success, error) in @@ -554,33 +722,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } }) } - + func delete(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) - - let predicate = HKQuery.predicateForSamples( + + let samplePredicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) + let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default()) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let deleteQuery = HKSampleQuery( - sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit, + sampleType: dataType, + predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]), + limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + guard let samplesOrNil = samplesOrNil, error == nil else { // Handle the error if necessary print("Error deleting \(dataType)") return } - + // Delete the retrieved objects from the HealthKit store HKHealthStore().delete(samplesOrNil) { (success, error) in if let err = error { @@ -591,10 +762,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(deleteQuery) } - + func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String)! @@ -602,18 +773,66 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit - let includeManualEntry = (arguments?["includeManualEntry"] as? Bool) ?? true - + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let dataType = dataTypeLookUp(key: dataTypeKey) var unit: HKUnit? if let dataUnitKey = dataUnitKey { unit = unitDict[dataUnitKey] } - + + let sourceIdForCharacteristic = "com.apple.Health" + let sourceNameForCharacteristic = "Health" + + switch(dataTypeKey) { + case "BIRTH_DATE": + let dateOfBirth = getBirthDate() + result([ + [ + "value": dateOfBirth?.timeIntervalSince1970, + "date_from": Int(dateFrom.timeIntervalSince1970 * 1000), + "date_to": Int(dateTo.timeIntervalSince1970 * 1000), + "source_id": sourceIdForCharacteristic, + "source_name": sourceNameForCharacteristic, + "recording_method": RecordingMethod.manual.rawValue + ] + ]) + return + case "GENDER": + let gender = getGender() + result([ + [ + "value": gender?.rawValue, + "date_from": Int(dateFrom.timeIntervalSince1970 * 1000), + "date_to": Int(dateTo.timeIntervalSince1970 * 1000), + "source_id": sourceIdForCharacteristic, + "source_name": sourceNameForCharacteristic, + "recording_method": RecordingMethod.manual.rawValue + ] + ]) + return + case "BLOOD_TYPE": + let bloodType = getBloodType() + result([ + [ + "value": bloodType?.rawValue, + "date_from": Int(dateFrom.timeIntervalSince1970 * 1000), + "date_to": Int(dateTo.timeIntervalSince1970 * 1000), + "source_id": sourceIdForCharacteristic, + "source_name": sourceNameForCharacteristic, + "recording_method": RecordingMethod.manual.rawValue + ] + ]) + return + default: + break + } + var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) if (!includeManualEntry) { @@ -621,48 +840,46 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) - + let query = HKSampleQuery( sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor] ) { [self] x, samplesOrNil, error in - + switch samplesOrNil { case let (samples as [HKQuantitySample]) as Any: let dictionaries = samples.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", - "value": sample.quantity.doubleValue(for: unit!), + "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue, + "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil, + "dataUnitKey": unit?.unitString ] } DispatchQueue.main.async { result(dictionaries) } - + case var (samplesCategory as [HKCategorySample]) as Any: - + if dataTypeKey == self.SLEEP_IN_BED { samplesCategory = samplesCategory.filter { $0.value == 0 } } - if dataTypeKey == self.SLEEP_ASLEEP_CORE { - samplesCategory = samplesCategory.filter { $0.value == 3 } - } - if dataTypeKey == self.SLEEP_ASLEEP_DEEP { - samplesCategory = samplesCategory.filter { $0.value == 4 } - } - if dataTypeKey == self.SLEEP_ASLEEP_REM { - samplesCategory = samplesCategory.filter { $0.value == 5 } + if dataTypeKey == self.SLEEP_ASLEEP { + samplesCategory = samplesCategory.filter { $0.value == 1 } } if dataTypeKey == self.SLEEP_AWAKE { samplesCategory = samplesCategory.filter { $0.value == 2 } } - if dataTypeKey == self.SLEEP_ASLEEP { + if dataTypeKey == self.SLEEP_LIGHT { samplesCategory = samplesCategory.filter { $0.value == 3 } } if dataTypeKey == self.SLEEP_DEEP { @@ -687,6 +904,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { samplesCategory = samplesCategory.filter { $0.value == 4 } } let categories = samplesCategory.map { sample -> NSDictionary in + var metadata: [String: Any] = [:] + + if let sampleMetadata = sample.metadata { + for (key, value) in sampleMetadata { + metadata[key] = value + } + } + return [ "uuid": "\(sample.uuid)", "value": sample.value, @@ -694,15 +919,16 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, + "metadata": metadata ] } DispatchQueue.main.async { result(categories) } - + case let (samplesWorkout as [HKWorkout]) as Any: - + let dictionaries = samplesWorkout.map { sample -> NSDictionary in return [ "uuid": "\(sample.uuid)", @@ -717,17 +943,17 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, "workout_type": self.getWorkoutType(type: sample.workoutActivityType), "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 ] } - + DispatchQueue.main.async { result(dictionaries) } - + case let (samplesAudiogram as [HKAudiogramSample]) as Any: let dictionaries = samplesAudiogram.map { sample -> NSDictionary in var frequencies = [Double]() @@ -754,55 +980,45 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { result(dictionaries) } - + case let (nutritionSample as [HKCorrelation]) as Any: - - //let samples = nutritionSample[0].objects(for: HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)!) - var calories = 0.0 - var fat = 0.0 - var carbs = 0.0 - var protein = 0.0 - - let name = nutritionSample[0].metadata?[HKMetadataKeyFoodType] as! String - let mealType = nutritionSample[0].metadata?["HKFoodMeal"] - let samples = nutritionSample[0].objects - for sample in samples { - if let quantitySample = sample as? HKQuantitySample { - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)){ - calories = quantitySample.quantity.doubleValue(for: HKUnit.kilocalorie()) - } - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)){ - carbs = quantitySample.quantity.doubleValue(for: HKUnit.gram()) - } - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryProtein)){ - protein = quantitySample.quantity.doubleValue(for: HKUnit.gram()) - } - if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)){ - fat = quantitySample.quantity.doubleValue(for: HKUnit.gram()) + var foods: [[String: Any?]] = [] + for food in nutritionSample { + let name = food.metadata?[HKMetadataKeyFoodType] as? String + let mealType = food.metadata?["HKFoodMeal"] + let samples = food.objects + // get first sample if it exists + if let sample = samples.first as? HKQuantitySample { + var sampleDict = [ + "uuid": "\(sample.uuid)", + "name": name, + "meal_type": mealType, + "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), + "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), + "source_id": sample.sourceRevision.source.bundleIdentifier, + "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue + ] + for sample in samples { + if let quantitySample = sample as? HKQuantitySample { + for (key, identifier) in NUTRITION_KEYS { + if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){ + let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram() + sampleDict[key] = quantitySample.quantity.doubleValue(for: unit) + } + } + } } + foods.append(sampleDict) } } - - - let dictionaries = nutritionSample.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "calories": calories, - "carbs": carbs, - "protein": protein, - "fat": fat, - "name": name, - "mealType": mealType, - "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000), - "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), - "source_id": sample.sourceRevision.source.bundleIdentifier, - "source_name": sample.sourceRevision.source.name, - ] - } + DispatchQueue.main.async { - result(dictionaries) + result(foods) } - + default: if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { let dictionaries = ecgSamples.map(fetchEcgMeasurements) @@ -817,10 +1033,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } } - + HKHealthStore().execute(query) } - + @available(iOS 14.0, *) private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { let semaphore = DispatchSemaphore(value: 0) @@ -854,7 +1070,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "source_name": sample.sourceRevision.source.name, ] } - + func getIntervalData(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let dataTypeKey = (arguments?["dataTypeKey"] as? String) ?? "DEFAULT" @@ -862,25 +1078,26 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let startDate = (arguments?["startTime"] as? NSNumber) ?? 0 let endDate = (arguments?["endTime"] as? NSNumber) ?? 0 let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 - let includeManualEntry = (arguments?["includeManualEntry"] as? Bool) ?? true - + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + // Set interval in seconds. var interval = DateComponents() interval.second = intervalInSecond - + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startDate.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endDate.doubleValue / 1000) - + let quantityType: HKQuantityType! = dataQuantityTypesDict[dataTypeKey] var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: []) if (!includeManualEntry) { let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) } - + let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: [.cumulativeSum, .separateBySource], anchorDate: dateFrom, intervalComponents: interval) - + query.initialResultsHandler = { [weak self] _, statisticCollectionOrNil, error in guard let self = self else { @@ -891,7 +1108,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + // Error detected. if let error = error { print("Query error: \(error.localizedDescription)") @@ -900,7 +1117,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + guard let collection = statisticCollectionOrNil as? HKStatisticsCollection else { print("Unexpected result from query") DispatchQueue.main.async { @@ -908,7 +1125,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } return } - + var dictionaries = [[String: Any]]() collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in @@ -917,7 +1134,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { print("Self is nil during enumeration") return } - + do { if let quantity = statisticData.sumQuantity(), let dataUnitKey = dataUnitKey, @@ -941,27 +1158,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } HKHealthStore().execute(query) } - + func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - + let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)! - let predicate = HKQuery.predicateForSamples( + var predicate = HKQuery.predicateForSamples( withStart: dateFrom, end: dateTo, options: .strictStartDate) - - let query = HKStatisticsQuery( + if (!includeManualEntry) { + let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered) + predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate]) + } + + // TODO: [NOTE] Computational heavy + let query = HKStatisticsCollectionQuery( quantityType: sampleType, quantitySamplePredicate: predicate, - options: .cumulativeSum - ) { query, queryResult, error in - - guard let queryResult = queryResult else { + options: .cumulativeSum, + anchorDate: dateFrom, + intervalComponents: DateComponents(day: 1) + ) + query.initialResultsHandler = { query, results, error in + guard let results = results else { let error = error! as NSError print("Error getting total steps in interval \(error.localizedDescription)") @@ -969,38 +1195,71 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { result(nil) } return - } - - var steps = 0.0 + } - if let quantity = queryResult.sumQuantity() { - let unit = HKUnit.count() - steps = quantity.doubleValue(for: unit) - } + var totalSteps = 0.0 + results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in + if let quantity = statistics.sumQuantity() { + let unit = HKUnit.count() + totalSteps += quantity.doubleValue(for: unit) + } + } - let totalSteps = Int(steps) - DispatchQueue.main.async { - result(totalSteps) - } + DispatchQueue.main.async { + result(Int(totalSteps)) + } } - + HKHealthStore().execute(query) } - + func unitLookUp(key: String) -> HKUnit { guard let unit = unitDict[key] else { return HKUnit.count() } return unit } - + func dataTypeLookUp(key: String) -> HKSampleType { guard let dataType_ = dataTypesDict[key] else { return HKSampleType.quantityType(forIdentifier: .bodyMass)! } return dataType_ } - + + func getGender() -> HKBiologicalSex? { + var bioSex:HKBiologicalSex? + do { + bioSex = try healthStore.biologicalSex().biologicalSex + } catch { + bioSex = nil + print("Error retrieving biologicalSex: \(error)") + } + return bioSex + } + + func getBirthDate() -> Date? { + var dob:Date? + do { + dob = try healthStore.dateOfBirthComponents().date + } catch { + dob = nil + print("Error retrieving date of birth: \(error)") + } + return dob + } + + func getBloodType() -> HKBloodType? { + var bloodType:HKBloodType? + do { + bloodType = try healthStore.bloodType().bloodType + } catch { + bloodType = nil + print("Error retrieving blood type: \(error)") + } + return bloodType + } + func initializeTypes() { // Initialize units unitDict[GRAM] = HKUnit.gram() @@ -1049,7 +1308,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL") unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "") unitDict[NO_UNIT] = HKUnit.init(from: "") - + // Initialize workout types workoutActivityTypeMap["ARCHERY"] = .archery workoutActivityTypeMap["BOWLING"] = .bowling @@ -1073,8 +1332,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["FLEXIBILITY"] = .flexibility workoutActivityTypeMap["WALKING"] = .walking workoutActivityTypeMap["RUNNING"] = .running - workoutActivityTypeMap["RUNNING_JOGGING"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["RUNNING_SAND"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["RUNNING_TREADMILL"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["WHEELCHAIR_WALK_PACE"] = .wheelchairWalkPace workoutActivityTypeMap["WHEELCHAIR_RUN_PACE"] = .wheelchairRunPace @@ -1115,14 +1372,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["SNOW_SPORTS"] = .snowSports workoutActivityTypeMap["SNOWBOARDING"] = .snowboarding workoutActivityTypeMap["SKATING"] = .skatingSports - workoutActivityTypeMap["SKATING_CROSS,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INDOOR,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INLINE,"] = .skatingSports // Supported due to combining with Android naming workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports workoutActivityTypeMap["ROWING"] = .rowing workoutActivityTypeMap["SAILING"] = .sailing - workoutActivityTypeMap["SURFING_SPORTS"] = .surfingSports + workoutActivityTypeMap["SURFING"] = .surfingSports workoutActivityTypeMap["SWIMMING"] = .swimming + workoutActivityTypeMap["SWIMMING_OPEN_WATER"] = .swimming + workoutActivityTypeMap["SWIMMING_POOL"] = .swimming workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness workoutActivityTypeMap["WATER_POLO"] = .waterPolo workoutActivityTypeMap["WATER_SPORTS"] = .waterSports @@ -1132,7 +1388,22 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["TAI_CHI"] = .taiChi workoutActivityTypeMap["WRESTLING"] = .wrestling workoutActivityTypeMap["OTHER"] = .other + if #available(iOS 17.0, *) { + workoutActivityTypeMap["UNDERWATER_DIVING"] = .underwaterDiving + } + nutritionList = [ + DIETARY_ENERGY_CONSUMED, DIETARY_CARBS_CONSUMED, DIETARY_PROTEIN_CONSUMED, + DIETARY_FATS_CONSUMED, DIETARY_CAFFEINE, DIETARY_FIBER, DIETARY_SUGAR, + DIETARY_FAT_MONOUNSATURATED, DIETARY_FAT_POLYUNSATURATED, DIETARY_FAT_SATURATED, + DIETARY_CHOLESTEROL, DIETARY_VITAMIN_A, DIETARY_THIAMIN, DIETARY_RIBOFLAVIN, + DIETARY_NIACIN, DIETARY_PANTOTHENIC_ACID, DIETARY_VITAMIN_B6, DIETARY_BIOTIN, + DIETARY_VITAMIN_B12, DIETARY_VITAMIN_C, DIETARY_VITAMIN_D, DIETARY_VITAMIN_E, + DIETARY_VITAMIN_K, DIETARY_FOLATE, DIETARY_CALCIUM, DIETARY_CHLORIDE, + DIETARY_IRON, DIETARY_MAGNESIUM, DIETARY_PHOSPHORUS, DIETARY_POTASSIUM, + DIETARY_SODIUM, DIETARY_ZINC, DIETARY_WATER, DIETARY_CHROMIUM, DIETARY_COPPER, + DIETARY_IODINE, DIETARY_MANGANESE, DIETARY_MOLYBDENUM, DIETARY_SELENIUM, + ] // Set up iOS 13 specific types (ordinary health data types) if #available(iOS 13.0, *) { dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType( @@ -1145,25 +1416,58 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)! dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType( forIdentifier: .peripheralPerfusionIndex)! - + dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType( forIdentifier: .bloodPressureDiastolic)! dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType( forIdentifier: .bloodPressureSystolic)! dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType( forIdentifier: .bodyFatPercentage)! + dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)! dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)! - dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryCarbohydrates)! - dataTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryEnergyConsumed)! - dataTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType( - forIdentifier: .dietaryCaffeine)! - dataTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryFatTotal)! - dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType( - forIdentifier: .dietaryProtein)! + + // Nutrition + dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! + dataTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! + dataTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)! + dataTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)! + dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)! + dataTypesDict[DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)! + dataTypesDict[DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)! + dataTypesDict[DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)! + dataTypesDict[DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)! + dataTypesDict[DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)! + dataTypesDict[DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)! + dataTypesDict[DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)! + dataTypesDict[DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)! + dataTypesDict[DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)! + dataTypesDict[DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)! + dataTypesDict[DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)! + dataTypesDict[DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)! + dataTypesDict[DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)! + dataTypesDict[DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)! + dataTypesDict[DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)! + dataTypesDict[DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)! + dataTypesDict[DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)! + dataTypesDict[DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)! + dataTypesDict[DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)! + dataTypesDict[DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)! + dataTypesDict[DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)! + dataTypesDict[DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)! + dataTypesDict[DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)! + dataTypesDict[DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)! + dataTypesDict[DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)! + dataTypesDict[DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)! + dataTypesDict[DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)! + dataTypesDict[DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)! + dataTypesDict[DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)! + dataTypesDict[DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)! + dataTypesDict[DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)! + dataTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! + dataTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! + dataTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! + dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( forIdentifier: .electrodermalActivity)! dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( @@ -1172,6 +1476,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType( forIdentifier: .heartRateVariabilitySDNN)! dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)! + dataTypesDict[INSULIN_DELIVERY] = HKSampleType.quantityType(forIdentifier: .insulinDelivery)! dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType( forIdentifier: .restingHeartRate)! dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)! @@ -1185,25 +1490,29 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[DISTANCE_SWIMMING] = HKSampleType.quantityType(forIdentifier: .distanceSwimming)! dataTypesDict[DISTANCE_CYCLING] = HKSampleType.quantityType(forIdentifier: .distanceCycling)! dataTypesDict[FLIGHTS_CLIMBED] = HKSampleType.quantityType(forIdentifier: .flightsClimbed)! - dataTypesDict[WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)! dataTypesDict[MINDFULNESS] = HKSampleType.categoryType(forIdentifier: .mindfulSession)! - dataTypesDict[SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_ASLEEP_CORE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_ASLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[SLEEP_ASLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[SLEEP_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_LIGHT] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - + dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)! + + dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! dataTypesDict[WORKOUT] = HKSampleType.workoutType() dataTypesDict[NUTRITION] = HKSampleType.correlationType( forIdentifier: .food)! - + healthDataTypes = Array(dataTypesDict.values) + + characteristicsTypesDict[BIRTH_DATE] = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)! + characteristicsTypesDict[GENDER] = HKObjectType.characteristicType(forIdentifier: .biologicalSex)! + characteristicsTypesDict[BLOOD_TYPE] = HKObjectType.characteristicType(forIdentifier: .bloodType)! + characteristicsDataTypes = Array(characteristicsTypesDict.values) } - + // Set up iOS 11 specific types (ordinary health data quantity types) if #available(iOS 11.0, *) { dataQuantityTypesDict[ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)! @@ -1213,12 +1522,51 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)! dataQuantityTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)! dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)! + dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)! dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)! dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)! - dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates)! - dataQuantityTypesDict[DIETARY_ENERGY_CONSUMED] = HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed)! - dataQuantityTypesDict[DIETARY_FATS_CONSUMED] = HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal)! - dataQuantityTypesDict[DIETARY_PROTEIN_CONSUMED] = HKQuantityType.quantityType(forIdentifier: .dietaryProtein)! + + // Nutrition + dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)! + dataQuantityTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)! + dataQuantityTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)! + dataQuantityTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)! + dataQuantityTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)! + dataQuantityTypesDict[DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)! + dataQuantityTypesDict[DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)! + dataQuantityTypesDict[DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)! + dataQuantityTypesDict[DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)! + dataQuantityTypesDict[DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)! + dataQuantityTypesDict[DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)! + dataQuantityTypesDict[DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)! + dataQuantityTypesDict[DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)! + dataQuantityTypesDict[DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)! + dataQuantityTypesDict[DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)! + dataQuantityTypesDict[DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)! + dataQuantityTypesDict[DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)! + dataQuantityTypesDict[DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)! + dataQuantityTypesDict[DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)! + dataQuantityTypesDict[DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)! + dataQuantityTypesDict[DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)! + dataQuantityTypesDict[DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)! + dataQuantityTypesDict[DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)! + dataQuantityTypesDict[DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)! + dataQuantityTypesDict[DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)! + dataQuantityTypesDict[DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)! + dataQuantityTypesDict[DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)! + dataQuantityTypesDict[DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)! + dataQuantityTypesDict[DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)! + dataQuantityTypesDict[DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)! + dataQuantityTypesDict[DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)! + dataQuantityTypesDict[DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)! + dataQuantityTypesDict[DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)! + dataQuantityTypesDict[DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)! + dataQuantityTypesDict[DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)! + dataQuantityTypesDict[DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)! + dataQuantityTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)! + dataQuantityTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)! + dataQuantityTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)! + dataQuantityTypesDict[ELECTRODERMAL_ACTIVITY] = HKQuantityType.quantityType(forIdentifier: .electrodermalActivity)! dataQuantityTypesDict[FORCED_EXPIRATORY_VOLUME] = HKQuantityType.quantityType(forIdentifier: .forcedExpiratoryVolume1)! dataQuantityTypesDict[HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .heartRate)! @@ -1233,11 +1581,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataQuantityTypesDict[DISTANCE_SWIMMING] = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)! dataQuantityTypesDict[DISTANCE_CYCLING] = HKQuantityType.quantityType(forIdentifier: .distanceCycling)! dataQuantityTypesDict[FLIGHTS_CLIMBED] = HKQuantityType.quantityType(forIdentifier: .flightsClimbed)! - dataQuantityTypesDict[WATER] = HKQuantityType.quantityType(forIdentifier: .dietaryWater)! - + healthDataQuantityTypes = Array(dataQuantityTypesDict.values) } - + // Set up heart rate data types specific to the apple watch, requires iOS 12 if #available(iOS 12.2, *) { dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType( @@ -1246,32 +1593,32 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { forIdentifier: .lowHeartRateEvent)! dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType( forIdentifier: .irregularHeartRhythmEvent)! - + heartRateEventTypes = Set([ HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!, HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!, ]) } - + if #available(iOS 13.6, *) { dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)! dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)! - + headacheType = Set([ HKSampleType.categoryType(forIdentifier: .headache)! ]) } - + if #available(iOS 14.0, *) { dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType() - + unitDict[VOLT] = HKUnit.volt() unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury() - + workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball @@ -1282,11 +1629,18 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { dataTypesDict[TIME_IN_DAYLIGHT] = HKObjectType.quantityType(forIdentifier: .timeInDaylight)! } + if #available(iOS 16.0, *) { + dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)! + + dataTypesDict[WATER_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .waterTemperature)! + dataTypesDict[UNDERWATER_DEPTH] = HKQuantityType.quantityType(forIdentifier: .underwaterDepth)! + } + // Concatenate heart events, headache and health data types (both may be empty) allDataTypes = Set(heartRateEventTypes + healthDataTypes) allDataTypes = allDataTypes.union(headacheType) } - + func getWorkoutType(type: HKWorkoutActivityType) -> String { switch type { case .americanFootball: @@ -1437,8 +1791,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { return "mixedCardio" case .handCycling: return "handCycling" + case .underwaterDiving: + return "underwaterDiving" default: return "other" } - } + } } diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 443b44c7c..96254602b 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -3,14 +3,14 @@ # Pod::Spec.new do |s| s.name = 'health' - s.version = '1.0.4' - s.summary = 'Wrapper for the iOS HealthKit and Android GoogleFit services.' + s.version = '12.0.0' + s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC -Wrapper for the iOS HealthKit and Android GoogleFit services. +Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. DESC s.homepage = 'https://pub.dev/packages/health' s.license = { :file => '../LICENSE' } - s.author = { 'Copenhagen Center for Health Technology' => 'cph.cachet@gmail.com' } + s.author = { 'Copenhagen Research Platform at DTU' => 'support@carp.dk' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' diff --git a/packages/health/lib/health.dart b/packages/health/lib/health.dart index 1c960d54f..af94d80e7 100644 --- a/packages/health/lib/health.dart +++ b/packages/health/lib/health.dart @@ -1,4 +1,4 @@ -library health; +library; import 'dart:async'; import 'dart:collection'; diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 8b23ccc0e..70feb2de1 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -8,49 +8,48 @@ part of 'health.dart'; HealthDataPoint _$HealthDataPointFromJson(Map json) => HealthDataPoint( + uuid: json['uuid'] as String, value: HealthValue.fromJson(json['value'] as Map), type: $enumDecode(_$HealthDataTypeEnumMap, json['type']), unit: $enumDecode(_$HealthDataUnitEnumMap, json['unit']), - dateFrom: DateTime.parse(json['date_from'] as String), - dateTo: DateTime.parse(json['date_to'] as String), + dateFrom: DateTime.parse(json['dateFrom'] as String), + dateTo: DateTime.parse(json['dateTo'] as String), sourcePlatform: - $enumDecode(_$HealthPlatformTypeEnumMap, json['source_platform']), - sourceDeviceId: json['source_device_id'] as String, - sourceId: json['source_id'] as String, - sourceName: json['source_name'] as String, - isManualEntry: json['is_manual_entry'] as bool? ?? false, - workoutSummary: json['workout_summary'] == null + $enumDecode(_$HealthPlatformTypeEnumMap, json['sourcePlatform']), + sourceDeviceId: json['sourceDeviceId'] as String, + sourceId: json['sourceId'] as String, + sourceName: json['sourceName'] as String, + recordingMethod: $enumDecodeNullable( + _$RecordingMethodEnumMap, json['recordingMethod']) ?? + RecordingMethod.unknown, + workoutSummary: json['workoutSummary'] == null ? null : WorkoutSummary.fromJson( - json['workout_summary'] as Map), + json['workoutSummary'] as Map), + metadata: json['metadata'] as Map?, ); -Map _$HealthDataPointToJson(HealthDataPoint instance) { - final val = { - 'value': instance.value, - 'type': _$HealthDataTypeEnumMap[instance.type]!, - 'unit': _$HealthDataUnitEnumMap[instance.unit]!, - 'date_from': instance.dateFrom.toIso8601String(), - 'date_to': instance.dateTo.toIso8601String(), - 'source_platform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, - 'source_device_id': instance.sourceDeviceId, - 'source_id': instance.sourceId, - 'source_name': instance.sourceName, - 'is_manual_entry': instance.isManualEntry, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('workout_summary', instance.workoutSummary); - return val; -} +Map _$HealthDataPointToJson(HealthDataPoint instance) => + { + 'uuid': instance.uuid, + 'value': instance.value.toJson(), + 'type': _$HealthDataTypeEnumMap[instance.type]!, + 'unit': _$HealthDataUnitEnumMap[instance.unit]!, + 'dateFrom': instance.dateFrom.toIso8601String(), + 'dateTo': instance.dateTo.toIso8601String(), + 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'sourceDeviceId': instance.sourceDeviceId, + 'sourceId': instance.sourceId, + 'sourceName': instance.sourceName, + 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, + if (instance.workoutSummary?.toJson() case final value?) + 'workoutSummary': value, + if (instance.metadata case final value?) 'metadata': value, + }; const _$HealthDataTypeEnumMap = { HealthDataType.ACTIVE_ENERGY_BURNED: 'ACTIVE_ENERGY_BURNED', + HealthDataType.ATRIAL_FIBRILLATION_BURDEN: 'ATRIAL_FIBRILLATION_BURDEN', HealthDataType.AUDIOGRAM: 'AUDIOGRAM', HealthDataType.BASAL_ENERGY_BURNED: 'BASAL_ENERGY_BURNED', HealthDataType.BLOOD_GLUCOSE: 'BLOOD_GLUCOSE', @@ -58,6 +57,7 @@ const _$HealthDataTypeEnumMap = { HealthDataType.BLOOD_PRESSURE_DIASTOLIC: 'BLOOD_PRESSURE_DIASTOLIC', HealthDataType.BLOOD_PRESSURE_SYSTOLIC: 'BLOOD_PRESSURE_SYSTOLIC', HealthDataType.BODY_FAT_PERCENTAGE: 'BODY_FAT_PERCENTAGE', + HealthDataType.LEAN_BODY_MASS: 'LEAN_BODY_MASS', HealthDataType.BODY_MASS_INDEX: 'BODY_MASS_INDEX', HealthDataType.BODY_TEMPERATURE: 'BODY_TEMPERATURE', HealthDataType.BODY_WATER_MASS: 'BODY_WATER_MASS', @@ -66,10 +66,45 @@ const _$HealthDataTypeEnumMap = { HealthDataType.DIETARY_ENERGY_CONSUMED: 'DIETARY_ENERGY_CONSUMED', HealthDataType.DIETARY_FATS_CONSUMED: 'DIETARY_FATS_CONSUMED', HealthDataType.DIETARY_PROTEIN_CONSUMED: 'DIETARY_PROTEIN_CONSUMED', + HealthDataType.DIETARY_FIBER: 'DIETARY_FIBER', + HealthDataType.DIETARY_SUGAR: 'DIETARY_SUGAR', + HealthDataType.DIETARY_FAT_MONOUNSATURATED: 'DIETARY_FAT_MONOUNSATURATED', + HealthDataType.DIETARY_FAT_POLYUNSATURATED: 'DIETARY_FAT_POLYUNSATURATED', + HealthDataType.DIETARY_FAT_SATURATED: 'DIETARY_FAT_SATURATED', + HealthDataType.DIETARY_CHOLESTEROL: 'DIETARY_CHOLESTEROL', + HealthDataType.DIETARY_VITAMIN_A: 'DIETARY_VITAMIN_A', + HealthDataType.DIETARY_THIAMIN: 'DIETARY_THIAMIN', + HealthDataType.DIETARY_RIBOFLAVIN: 'DIETARY_RIBOFLAVIN', + HealthDataType.DIETARY_NIACIN: 'DIETARY_NIACIN', + HealthDataType.DIETARY_PANTOTHENIC_ACID: 'DIETARY_PANTOTHENIC_ACID', + HealthDataType.DIETARY_VITAMIN_B6: 'DIETARY_VITAMIN_B6', + HealthDataType.DIETARY_BIOTIN: 'DIETARY_BIOTIN', + HealthDataType.DIETARY_VITAMIN_B12: 'DIETARY_VITAMIN_B12', + HealthDataType.DIETARY_VITAMIN_C: 'DIETARY_VITAMIN_C', + HealthDataType.DIETARY_VITAMIN_D: 'DIETARY_VITAMIN_D', + HealthDataType.DIETARY_VITAMIN_E: 'DIETARY_VITAMIN_E', + HealthDataType.DIETARY_VITAMIN_K: 'DIETARY_VITAMIN_K', + HealthDataType.DIETARY_FOLATE: 'DIETARY_FOLATE', + HealthDataType.DIETARY_CALCIUM: 'DIETARY_CALCIUM', + HealthDataType.DIETARY_CHLORIDE: 'DIETARY_CHLORIDE', + HealthDataType.DIETARY_IRON: 'DIETARY_IRON', + HealthDataType.DIETARY_MAGNESIUM: 'DIETARY_MAGNESIUM', + HealthDataType.DIETARY_PHOSPHORUS: 'DIETARY_PHOSPHORUS', + HealthDataType.DIETARY_POTASSIUM: 'DIETARY_POTASSIUM', + HealthDataType.DIETARY_SODIUM: 'DIETARY_SODIUM', + HealthDataType.DIETARY_ZINC: 'DIETARY_ZINC', + HealthDataType.DIETARY_CHROMIUM: 'DIETARY_CHROMIUM', + HealthDataType.DIETARY_COPPER: 'DIETARY_COPPER', + HealthDataType.DIETARY_IODINE: 'DIETARY_IODINE', + HealthDataType.DIETARY_MANGANESE: 'DIETARY_MANGANESE', + HealthDataType.DIETARY_MOLYBDENUM: 'DIETARY_MOLYBDENUM', + HealthDataType.DIETARY_SELENIUM: 'DIETARY_SELENIUM', HealthDataType.FORCED_EXPIRATORY_VOLUME: 'FORCED_EXPIRATORY_VOLUME', HealthDataType.HEART_RATE: 'HEART_RATE', HealthDataType.HEART_RATE_VARIABILITY_SDNN: 'HEART_RATE_VARIABILITY_SDNN', + HealthDataType.HEART_RATE_VARIABILITY_RMSSD: 'HEART_RATE_VARIABILITY_RMSSD', HealthDataType.HEIGHT: 'HEIGHT', + HealthDataType.INSULIN_DELIVERY: 'INSULIN_DELIVERY', HealthDataType.RESTING_HEART_RATE: 'RESTING_HEART_RATE', HealthDataType.RESPIRATORY_RATE: 'RESPIRATORY_RATE', HealthDataType.PERIPHERAL_PERFUSION_INDEX: 'PERIPHERAL_PERFUSION_INDEX', @@ -81,21 +116,19 @@ const _$HealthDataTypeEnumMap = { HealthDataType.DISTANCE_SWIMMING: 'DISTANCE_SWIMMING', HealthDataType.DISTANCE_CYCLING: 'DISTANCE_CYCLING', HealthDataType.FLIGHTS_CLIMBED: 'FLIGHTS_CLIMBED', - HealthDataType.MOVE_MINUTES: 'MOVE_MINUTES', HealthDataType.DISTANCE_DELTA: 'DISTANCE_DELTA', HealthDataType.MINDFULNESS: 'MINDFULNESS', HealthDataType.WATER: 'WATER', - HealthDataType.SLEEP_IN_BED: 'SLEEP_IN_BED', HealthDataType.SLEEP_ASLEEP: 'SLEEP_ASLEEP', - HealthDataType.SLEEP_ASLEEP_CORE: 'SLEEP_ASLEEP_CORE', - HealthDataType.SLEEP_ASLEEP_DEEP: 'SLEEP_ASLEEP_DEEP', - HealthDataType.SLEEP_ASLEEP_REM: 'SLEEP_ASLEEP_REM', + HealthDataType.SLEEP_AWAKE_IN_BED: 'SLEEP_AWAKE_IN_BED', HealthDataType.SLEEP_AWAKE: 'SLEEP_AWAKE', - HealthDataType.SLEEP_LIGHT: 'SLEEP_LIGHT', HealthDataType.SLEEP_DEEP: 'SLEEP_DEEP', - HealthDataType.SLEEP_REM: 'SLEEP_REM', + HealthDataType.SLEEP_IN_BED: 'SLEEP_IN_BED', + HealthDataType.SLEEP_LIGHT: 'SLEEP_LIGHT', HealthDataType.SLEEP_OUT_OF_BED: 'SLEEP_OUT_OF_BED', + HealthDataType.SLEEP_REM: 'SLEEP_REM', HealthDataType.SLEEP_SESSION: 'SLEEP_SESSION', + HealthDataType.SLEEP_UNKNOWN: 'SLEEP_UNKNOWN', HealthDataType.EXERCISE_TIME: 'EXERCISE_TIME', HealthDataType.WORKOUT: 'WORKOUT', HealthDataType.HEADACHE_NOT_PRESENT: 'HEADACHE_NOT_PRESENT', @@ -104,6 +137,12 @@ const _$HealthDataTypeEnumMap = { HealthDataType.HEADACHE_SEVERE: 'HEADACHE_SEVERE', HealthDataType.HEADACHE_UNSPECIFIED: 'HEADACHE_UNSPECIFIED', HealthDataType.NUTRITION: 'NUTRITION', + HealthDataType.GENDER: 'GENDER', + HealthDataType.BIRTH_DATE: 'BIRTH_DATE', + HealthDataType.BLOOD_TYPE: 'BLOOD_TYPE', + HealthDataType.MENSTRUATION_FLOW: 'MENSTRUATION_FLOW', + HealthDataType.WATER_TEMPERATURE: 'WATER_TEMPERATURE', + HealthDataType.UNDERWATER_DEPTH: 'UNDERWATER_DEPTH', HealthDataType.HIGH_HEART_RATE_EVENT: 'HIGH_HEART_RATE_EVENT', HealthDataType.LOW_HEART_RATE_EVENT: 'LOW_HEART_RATE_EVENT', HealthDataType.IRREGULAR_HEART_RATE_EVENT: 'IRREGULAR_HEART_RATE_EVENT', @@ -166,128 +205,105 @@ const _$HealthDataUnitEnumMap = { const _$HealthPlatformTypeEnumMap = { HealthPlatformType.appleHealth: 'appleHealth', - HealthPlatformType.googleFit: 'googleFit', HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; +const _$RecordingMethodEnumMap = { + RecordingMethod.unknown: 'unknown', + RecordingMethod.active: 'active', + RecordingMethod.automatic: 'automatic', + RecordingMethod.manual: 'manual', +}; + HealthValue _$HealthValueFromJson(Map json) => HealthValue()..$type = json['__type'] as String?; -Map _$HealthValueToJson(HealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - return val; -} +Map _$HealthValueToJson(HealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + }; NumericHealthValue _$NumericHealthValueFromJson(Map json) => NumericHealthValue( - numericValue: json['numeric_value'] as num, + numericValue: json['numericValue'] as num, )..$type = json['__type'] as String?; -Map _$NumericHealthValueToJson(NumericHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['numeric_value'] = instance.numericValue; - return val; -} +Map _$NumericHealthValueToJson(NumericHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'numericValue': instance.numericValue, + }; AudiogramHealthValue _$AudiogramHealthValueFromJson( Map json) => AudiogramHealthValue( frequencies: (json['frequencies'] as List).map((e) => e as num).toList(), - leftEarSensitivities: (json['left_ear_sensitivities'] as List) + leftEarSensitivities: (json['leftEarSensitivities'] as List) .map((e) => e as num) .toList(), - rightEarSensitivities: (json['right_ear_sensitivities'] as List) + rightEarSensitivities: (json['rightEarSensitivities'] as List) .map((e) => e as num) .toList(), )..$type = json['__type'] as String?; Map _$AudiogramHealthValueToJson( - AudiogramHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['frequencies'] = instance.frequencies; - val['left_ear_sensitivities'] = instance.leftEarSensitivities; - val['right_ear_sensitivities'] = instance.rightEarSensitivities; - return val; -} + AudiogramHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'frequencies': instance.frequencies, + 'leftEarSensitivities': instance.leftEarSensitivities, + 'rightEarSensitivities': instance.rightEarSensitivities, + }; WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => WorkoutHealthValue( workoutActivityType: $enumDecode( - _$HealthWorkoutActivityTypeEnumMap, json['workout_activity_type']), - totalEnergyBurned: json['total_energy_burned'] as int?, + _$HealthWorkoutActivityTypeEnumMap, json['workoutActivityType']), + totalEnergyBurned: (json['totalEnergyBurned'] as num?)?.toInt(), totalEnergyBurnedUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_energy_burned_unit']), - totalDistance: json['total_distance'] as int?, + _$HealthDataUnitEnumMap, json['totalEnergyBurnedUnit']), + totalDistance: (json['totalDistance'] as num?)?.toInt(), totalDistanceUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_distance_unit']), - totalSteps: json['total_steps'] as int?, - totalStepsUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_steps_unit']), + _$HealthDataUnitEnumMap, json['totalDistanceUnit']), + totalSteps: (json['totalSteps'] as num?)?.toInt(), + totalStepsUnit: + $enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']), )..$type = json['__type'] as String?; -Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['workout_activity_type'] = - _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!; - writeNotNull('total_energy_burned', instance.totalEnergyBurned); - writeNotNull('total_energy_burned_unit', - _$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit]); - writeNotNull('total_distance', instance.totalDistance); - writeNotNull('total_distance_unit', - _$HealthDataUnitEnumMap[instance.totalDistanceUnit]); - writeNotNull('total_steps', instance.totalSteps); - writeNotNull( - 'total_steps_unit', _$HealthDataUnitEnumMap[instance.totalStepsUnit]); - return val; -} +Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'workoutActivityType': + _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!, + if (instance.totalEnergyBurned case final value?) + 'totalEnergyBurned': value, + if (_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit] + case final value?) + 'totalEnergyBurnedUnit': value, + if (instance.totalDistance case final value?) 'totalDistance': value, + if (_$HealthDataUnitEnumMap[instance.totalDistanceUnit] case final value?) + 'totalDistanceUnit': value, + if (instance.totalSteps case final value?) 'totalSteps': value, + if (_$HealthDataUnitEnumMap[instance.totalStepsUnit] case final value?) + 'totalStepsUnit': value, + }; const _$HealthWorkoutActivityTypeEnumMap = { + HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', HealthWorkoutActivityType.ARCHERY: 'ARCHERY', + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', HealthWorkoutActivityType.BADMINTON: 'BADMINTON', HealthWorkoutActivityType.BASEBALL: 'BASEBALL', HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', HealthWorkoutActivityType.BIKING: 'BIKING', HealthWorkoutActivityType.BOXING: 'BOXING', HealthWorkoutActivityType.CRICKET: 'CRICKET', + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', HealthWorkoutActivityType.CURLING: 'CURLING', + HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', HealthWorkoutActivityType.ELLIPTICAL: 'ELLIPTICAL', HealthWorkoutActivityType.FENCING: 'FENCING', - HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', - HealthWorkoutActivityType.SOCCER: 'SOCCER', HealthWorkoutActivityType.GOLF: 'GOLF', HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', HealthWorkoutActivityType.HANDBALL: 'HANDBALL', @@ -295,7 +311,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { 'HIGH_INTENSITY_INTERVAL_TRAINING', HealthWorkoutActivityType.HIKING: 'HIKING', HealthWorkoutActivityType.HOCKEY: 'HOCKEY', - HealthWorkoutActivityType.SKATING: 'SKATING', HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', @@ -305,9 +320,9 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.RUGBY: 'RUGBY', HealthWorkoutActivityType.RUNNING: 'RUNNING', HealthWorkoutActivityType.SAILING: 'SAILING', - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', - HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', + HealthWorkoutActivityType.SKATING: 'SKATING', HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', + HealthWorkoutActivityType.SOCCER: 'SOCCER', HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', HealthWorkoutActivityType.SQUASH: 'SQUASH', HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', @@ -318,147 +333,94 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.WALKING: 'WALKING', HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', HealthWorkoutActivityType.YOGA: 'YOGA', + HealthWorkoutActivityType.BARRE: 'BARRE', HealthWorkoutActivityType.BOWLING: 'BOWLING', + HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', + HealthWorkoutActivityType.CLIMBING: 'CLIMBING', + HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', + HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', - HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', - HealthWorkoutActivityType.LACROSSE: 'LACROSSE', - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: - 'PREPARATION_AND_RECOVERY', + HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', + HealthWorkoutActivityType.FISHING: 'FISHING', + HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', - HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', - HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', - HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: 'FUNCTIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: - 'TRADITIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', - HealthWorkoutActivityType.STAIRS: 'STAIRS', - HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', - HealthWorkoutActivityType.BARRE: 'BARRE', - HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', - HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', + HealthWorkoutActivityType.HUNTING: 'HUNTING', + HealthWorkoutActivityType.LACROSSE: 'LACROSSE', HealthWorkoutActivityType.MIND_AND_BODY: 'MIND_AND_BODY', + HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', + HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', - HealthWorkoutActivityType.CLIMBING: 'CLIMBING', - HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', - HealthWorkoutActivityType.FISHING: 'FISHING', - HealthWorkoutActivityType.HUNTING: 'HUNTING', HealthWorkoutActivityType.PLAY: 'PLAY', + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: + 'PREPARATION_AND_RECOVERY', HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', - HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', - HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', + HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.STAIRS: 'STAIRS', + HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', + HealthWorkoutActivityType.SURFING: 'SURFING', + HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', + HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', + HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: + 'TRADITIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', HealthWorkoutActivityType.WATER_SPORTS: 'WATER_SPORTS', - HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', HealthWorkoutActivityType.WRESTLING: 'WRESTLING', - HealthWorkoutActivityType.AEROBICS: 'AEROBICS', - HealthWorkoutActivityType.BIATHLON: 'BIATHLON', - HealthWorkoutActivityType.BIKING_HAND: 'BIKING_HAND', - HealthWorkoutActivityType.BIKING_MOUNTAIN: 'BIKING_MOUNTAIN', - HealthWorkoutActivityType.BIKING_ROAD: 'BIKING_ROAD', - HealthWorkoutActivityType.BIKING_SPINNING: 'BIKING_SPINNING', + HealthWorkoutActivityType.UNDERWATER_DIVING: 'UNDERWATER_DIVING', HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', - HealthWorkoutActivityType.BIKING_UTILITY: 'BIKING_UTILITY', HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', - HealthWorkoutActivityType.CIRCUIT_TRAINING: 'CIRCUIT_TRAINING', - HealthWorkoutActivityType.CROSS_FIT: 'CROSS_FIT', HealthWorkoutActivityType.DANCING: 'DANCING', - HealthWorkoutActivityType.DIVING: 'DIVING', - HealthWorkoutActivityType.ELEVATOR: 'ELEVATOR', - HealthWorkoutActivityType.ERGOMETER: 'ERGOMETER', - HealthWorkoutActivityType.ESCALATOR: 'ESCALATOR', HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', - HealthWorkoutActivityType.GARDENING: 'GARDENING', HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', - HealthWorkoutActivityType.HORSEBACK_RIDING: 'HORSEBACK_RIDING', - HealthWorkoutActivityType.HOUSEWORK: 'HOUSEWORK', - HealthWorkoutActivityType.INTERVAL_TRAINING: 'INTERVAL_TRAINING', - HealthWorkoutActivityType.IN_VEHICLE: 'IN_VEHICLE', HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', - HealthWorkoutActivityType.KAYAKING: 'KAYAKING', - HealthWorkoutActivityType.KETTLEBELL_TRAINING: 'KETTLEBELL_TRAINING', - HealthWorkoutActivityType.KICK_SCOOTER: 'KICK_SCOOTER', - HealthWorkoutActivityType.KITE_SURFING: 'KITE_SURFING', - HealthWorkoutActivityType.MEDITATION: 'MEDITATION', - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS: 'MIXED_MARTIAL_ARTS', - HealthWorkoutActivityType.P90X: 'P90X', HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', - HealthWorkoutActivityType.POLO: 'POLO', HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', - HealthWorkoutActivityType.RUNNING_JOGGING: 'RUNNING_JOGGING', - HealthWorkoutActivityType.RUNNING_SAND: 'RUNNING_SAND', HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', - HealthWorkoutActivityType.SKATING_CROSS: 'SKATING_CROSS', - HealthWorkoutActivityType.SKATING_INDOOR: 'SKATING_INDOOR', - HealthWorkoutActivityType.SKATING_INLINE: 'SKATING_INLINE', HealthWorkoutActivityType.SKIING: 'SKIING', - HealthWorkoutActivityType.SKIING_BACK_COUNTRY: 'SKIING_BACK_COUNTRY', - HealthWorkoutActivityType.SKIING_KITE: 'SKIING_KITE', - HealthWorkoutActivityType.SKIING_ROLLER: 'SKIING_ROLLER', - HealthWorkoutActivityType.SLEDDING: 'SLEDDING', - HealthWorkoutActivityType.SNOWMOBILE: 'SNOWMOBILE', HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING: 'STANDUP_PADDLEBOARDING', - HealthWorkoutActivityType.STILL: 'STILL', HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', - HealthWorkoutActivityType.SURFING: 'SURFING', HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', - HealthWorkoutActivityType.TEAM_SPORTS: 'TEAM_SPORTS', - HealthWorkoutActivityType.TILTING: 'TILTING', - HealthWorkoutActivityType.VOLLEYBALL_BEACH: 'VOLLEYBALL_BEACH', - HealthWorkoutActivityType.VOLLEYBALL_INDOOR: 'VOLLEYBALL_INDOOR', - HealthWorkoutActivityType.WAKEBOARDING: 'WAKEBOARDING', - HealthWorkoutActivityType.WALKING_FITNESS: 'WALKING_FITNESS', - HealthWorkoutActivityType.WALKING_NORDIC: 'WALKING_NORDIC', - HealthWorkoutActivityType.WALKING_STROLLER: 'WALKING_STROLLER', HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', - HealthWorkoutActivityType.WINDSURFING: 'WINDSURFING', - HealthWorkoutActivityType.ZUMBA: 'ZUMBA', HealthWorkoutActivityType.OTHER: 'OTHER', }; ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( Map json) => ElectrocardiogramHealthValue( - voltageValues: (json['voltage_values'] as List) + voltageValues: (json['voltageValues'] as List) .map((e) => ElectrocardiogramVoltageValue.fromJson(e as Map)) .toList(), - averageHeartRate: json['average_heart_rate'] as num?, - samplingFrequency: (json['sampling_frequency'] as num?)?.toDouble(), + averageHeartRate: json['averageHeartRate'] as num?, + samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), classification: $enumDecodeNullable( _$ElectrocardiogramClassificationEnumMap, json['classification']), )..$type = json['__type'] as String?; Map _$ElectrocardiogramHealthValueToJson( - ElectrocardiogramHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['voltage_values'] = instance.voltageValues; - writeNotNull('average_heart_rate', instance.averageHeartRate); - writeNotNull('sampling_frequency', instance.samplingFrequency); - writeNotNull('classification', - _$ElectrocardiogramClassificationEnumMap[instance.classification]); - return val; -} + ElectrocardiogramHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(), + if (instance.averageHeartRate case final value?) + 'averageHeartRate': value, + if (instance.samplingFrequency case final value?) + 'samplingFrequency': value, + if (_$ElectrocardiogramClassificationEnumMap[instance.classification] + case final value?) + 'classification': value, + }; const _$ElectrocardiogramClassificationEnumMap = { ElectrocardiogramClassification.NOT_SET: 'NOT_SET', @@ -478,70 +440,182 @@ ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( Map json) => ElectrocardiogramVoltageValue( voltage: json['voltage'] as num, - timeSinceSampleStart: json['time_since_sample_start'] as num, + timeSinceSampleStart: json['timeSinceSampleStart'] as num, )..$type = json['__type'] as String?; Map _$ElectrocardiogramVoltageValueToJson( - ElectrocardiogramVoltageValue instance) { - final val = {}; + ElectrocardiogramVoltageValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'voltage': instance.voltage, + 'timeSinceSampleStart': instance.timeSinceSampleStart, + }; - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } +InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson( + Map json) => + InsulinDeliveryHealthValue( + units: (json['units'] as num).toDouble(), + reason: $enumDecode(_$InsulinDeliveryReasonEnumMap, json['reason']), + )..$type = json['__type'] as String?; - writeNotNull('__type', instance.$type); - val['voltage'] = instance.voltage; - val['time_since_sample_start'] = instance.timeSinceSampleStart; - return val; -} +Map _$InsulinDeliveryHealthValueToJson( + InsulinDeliveryHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + 'units': instance.units, + 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!, + }; + +const _$InsulinDeliveryReasonEnumMap = { + InsulinDeliveryReason.NOT_SET: 'NOT_SET', + InsulinDeliveryReason.BASAL: 'BASAL', + InsulinDeliveryReason.BOLUS: 'BOLUS', +}; NutritionHealthValue _$NutritionHealthValueFromJson( Map json) => NutritionHealthValue( - mealType: json['meal_type'] as String?, - protein: (json['protein'] as num?)?.toDouble(), + name: json['name'] as String?, + mealType: json['mealType'] as String?, calories: (json['calories'] as num?)?.toDouble(), + protein: (json['protein'] as num?)?.toDouble(), fat: (json['fat'] as num?)?.toDouble(), - name: json['name'] as String?, carbs: (json['carbs'] as num?)?.toDouble(), caffeine: (json['caffeine'] as num?)?.toDouble(), + vitaminA: (json['vitaminA'] as num?)?.toDouble(), + b1Thiamine: (json['b1Thiamine'] as num?)?.toDouble(), + b2Riboflavin: (json['b2Riboflavin'] as num?)?.toDouble(), + b3Niacin: (json['b3Niacin'] as num?)?.toDouble(), + b5PantothenicAcid: (json['b5PantothenicAcid'] as num?)?.toDouble(), + b6Pyridoxine: (json['b6Pyridoxine'] as num?)?.toDouble(), + b7Biotin: (json['b7Biotin'] as num?)?.toDouble(), + b9Folate: (json['b9Folate'] as num?)?.toDouble(), + b12Cobalamin: (json['b12Cobalamin'] as num?)?.toDouble(), + vitaminC: (json['vitaminC'] as num?)?.toDouble(), + vitaminD: (json['vitaminD'] as num?)?.toDouble(), + vitaminE: (json['vitaminE'] as num?)?.toDouble(), + vitaminK: (json['vitaminK'] as num?)?.toDouble(), + calcium: (json['calcium'] as num?)?.toDouble(), + chloride: (json['chloride'] as num?)?.toDouble(), + cholesterol: (json['cholesterol'] as num?)?.toDouble(), + choline: (json['choline'] as num?)?.toDouble(), + chromium: (json['chromium'] as num?)?.toDouble(), + copper: (json['copper'] as num?)?.toDouble(), + fatUnsaturated: (json['fatUnsaturated'] as num?)?.toDouble(), + fatMonounsaturated: (json['fatMonounsaturated'] as num?)?.toDouble(), + fatPolyunsaturated: (json['fatPolyunsaturated'] as num?)?.toDouble(), + fatSaturated: (json['fatSaturated'] as num?)?.toDouble(), + fatTransMonoenoic: (json['fatTransMonoenoic'] as num?)?.toDouble(), + fiber: (json['fiber'] as num?)?.toDouble(), + iodine: (json['iodine'] as num?)?.toDouble(), + iron: (json['iron'] as num?)?.toDouble(), + magnesium: (json['magnesium'] as num?)?.toDouble(), + manganese: (json['manganese'] as num?)?.toDouble(), + molybdenum: (json['molybdenum'] as num?)?.toDouble(), + phosphorus: (json['phosphorus'] as num?)?.toDouble(), + potassium: (json['potassium'] as num?)?.toDouble(), + selenium: (json['selenium'] as num?)?.toDouble(), + sodium: (json['sodium'] as num?)?.toDouble(), + sugar: (json['sugar'] as num?)?.toDouble(), + water: (json['water'] as num?)?.toDouble(), + zinc: (json['zinc'] as num?)?.toDouble(), )..$type = json['__type'] as String?; Map _$NutritionHealthValueToJson( - NutritionHealthValue instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - writeNotNull('meal_type', instance.mealType); - writeNotNull('protein', instance.protein); - writeNotNull('calories', instance.calories); - writeNotNull('fat', instance.fat); - writeNotNull('name', instance.name); - writeNotNull('carbs', instance.carbs); - writeNotNull('caffeine', instance.caffeine); - return val; -} + NutritionHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + if (instance.name case final value?) 'name': value, + if (instance.mealType case final value?) 'mealType': value, + if (instance.calories case final value?) 'calories': value, + if (instance.protein case final value?) 'protein': value, + if (instance.fat case final value?) 'fat': value, + if (instance.carbs case final value?) 'carbs': value, + if (instance.caffeine case final value?) 'caffeine': value, + if (instance.vitaminA case final value?) 'vitaminA': value, + if (instance.b1Thiamine case final value?) 'b1Thiamine': value, + if (instance.b2Riboflavin case final value?) 'b2Riboflavin': value, + if (instance.b3Niacin case final value?) 'b3Niacin': value, + if (instance.b5PantothenicAcid case final value?) + 'b5PantothenicAcid': value, + if (instance.b6Pyridoxine case final value?) 'b6Pyridoxine': value, + if (instance.b7Biotin case final value?) 'b7Biotin': value, + if (instance.b9Folate case final value?) 'b9Folate': value, + if (instance.b12Cobalamin case final value?) 'b12Cobalamin': value, + if (instance.vitaminC case final value?) 'vitaminC': value, + if (instance.vitaminD case final value?) 'vitaminD': value, + if (instance.vitaminE case final value?) 'vitaminE': value, + if (instance.vitaminK case final value?) 'vitaminK': value, + if (instance.calcium case final value?) 'calcium': value, + if (instance.chloride case final value?) 'chloride': value, + if (instance.cholesterol case final value?) 'cholesterol': value, + if (instance.choline case final value?) 'choline': value, + if (instance.chromium case final value?) 'chromium': value, + if (instance.copper case final value?) 'copper': value, + if (instance.fatUnsaturated case final value?) 'fatUnsaturated': value, + if (instance.fatMonounsaturated case final value?) + 'fatMonounsaturated': value, + if (instance.fatPolyunsaturated case final value?) + 'fatPolyunsaturated': value, + if (instance.fatSaturated case final value?) 'fatSaturated': value, + if (instance.fatTransMonoenoic case final value?) + 'fatTransMonoenoic': value, + if (instance.fiber case final value?) 'fiber': value, + if (instance.iodine case final value?) 'iodine': value, + if (instance.iron case final value?) 'iron': value, + if (instance.magnesium case final value?) 'magnesium': value, + if (instance.manganese case final value?) 'manganese': value, + if (instance.molybdenum case final value?) 'molybdenum': value, + if (instance.phosphorus case final value?) 'phosphorus': value, + if (instance.potassium case final value?) 'potassium': value, + if (instance.selenium case final value?) 'selenium': value, + if (instance.sodium case final value?) 'sodium': value, + if (instance.sugar case final value?) 'sugar': value, + if (instance.water case final value?) 'water': value, + if (instance.zinc case final value?) 'zinc': value, + }; + +MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( + Map json) => + MenstruationFlowHealthValue( + flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), + dateTime: DateTime.parse(json['dateTime'] as String), + isStartOfCycle: json['isStartOfCycle'] as bool?, + wasUserEntered: json['wasUserEntered'] as bool?, + )..$type = json['__type'] as String?; + +Map _$MenstruationFlowHealthValueToJson( + MenstruationFlowHealthValue instance) => + { + if (instance.$type case final value?) '__type': value, + if (_$MenstrualFlowEnumMap[instance.flow] case final value?) + 'flow': value, + if (instance.isStartOfCycle case final value?) 'isStartOfCycle': value, + if (instance.wasUserEntered case final value?) 'wasUserEntered': value, + 'dateTime': instance.dateTime.toIso8601String(), + }; + +const _$MenstrualFlowEnumMap = { + MenstrualFlow.unspecified: 'unspecified', + MenstrualFlow.none: 'none', + MenstrualFlow.light: 'light', + MenstrualFlow.medium: 'medium', + MenstrualFlow.heavy: 'heavy', + MenstrualFlow.spotting: 'spotting', +}; WorkoutSummary _$WorkoutSummaryFromJson(Map json) => WorkoutSummary( - workoutType: json['workout_type'] as String, - totalDistance: json['total_distance'] as num, - totalEnergyBurned: json['total_energy_burned'] as num, - totalSteps: json['total_steps'] as num, + workoutType: json['workoutType'] as String, + totalDistance: json['totalDistance'] as num, + totalEnergyBurned: json['totalEnergyBurned'] as num, + totalSteps: json['totalSteps'] as num, ); Map _$WorkoutSummaryToJson(WorkoutSummary instance) => { - 'workout_type': instance.workoutType, - 'total_distance': instance.totalDistance, - 'total_energy_burned': instance.totalEnergyBurned, - 'total_steps': instance.totalSteps, + 'workoutType': instance.workoutType, + 'totalDistance': instance.totalDistance, + 'totalEnergyBurned': instance.totalEnergyBurned, + 'totalSteps': instance.totalSteps, }; diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart index 72a43924b..4694720ea 100644 --- a/packages/health/lib/health.json.dart +++ b/packages/health/lib/health.json.dart @@ -10,15 +10,20 @@ void _registerFromJsonFunctions() { FromJsonFactory().registerAll([ HealthValue(), NumericHealthValue(numericValue: 12), + WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.RUNNING), AudiogramHealthValue( frequencies: [], leftEarSensitivities: [], rightEarSensitivities: [], ), - WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.AEROBICS), ElectrocardiogramHealthValue(voltageValues: []), ElectrocardiogramVoltageValue(voltage: 12, timeSinceSampleStart: 0), NutritionHealthValue(), + MenstruationFlowHealthValue(flow: null, dateTime: DateTime.now()), + InsulinDeliveryHealthValue( + units: 0.0, + reason: InsulinDeliveryReason.NOT_SET, + ), ]); _fromJsonFunctionsRegistered = true; diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 3a6f65f95..029af835c 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -1,13 +1,16 @@ part of '../health.dart'; /// Types of health platforms. -enum HealthPlatformType { appleHealth, googleFit, googleHealthConnect } +enum HealthPlatformType { appleHealth, googleHealthConnect } /// A [HealthDataPoint] object corresponds to a data point capture from -/// Apple HealthKit or Google Fit or Google Health Connect with a [HealthValue] +/// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class HealthDataPoint { + /// UUID of the data point. + String uuid; + /// The quantity value of the data point HealthValue value; @@ -41,13 +44,19 @@ class HealthDataPoint { /// The name of the source from which the data point was fetched. String sourceName; - /// The user entered state of the data point. - bool isManualEntry; + /// How the data point was recorded + /// (on Android: https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/metadata/Metadata#summary) + /// on iOS: either user entered or manual https://developer.apple.com/documentation/healthkit/hkmetadatakeywasuserentered) + RecordingMethod recordingMethod; /// The summary of the workout data point, if available. WorkoutSummary? workoutSummary; + /// The metadata for this data point. + Map? metadata; + HealthDataPoint({ + required this.uuid, required this.value, required this.type, required this.unit, @@ -57,8 +66,9 @@ class HealthDataPoint { required this.sourceDeviceId, required this.sourceId, required this.sourceName, - this.isManualEntry = false, + this.recordingMethod = RecordingMethod.unknown, this.workoutSummary, + this.metadata, }) { // set the value to minutes rather than the category // returned by the native API @@ -68,12 +78,14 @@ class HealthDataPoint { type == HealthDataType.HEADACHE_MILD || type == HealthDataType.HEADACHE_MODERATE || type == HealthDataType.HEADACHE_SEVERE || - type == HealthDataType.SLEEP_IN_BED || type == HealthDataType.SLEEP_ASLEEP || type == HealthDataType.SLEEP_AWAKE || + type == HealthDataType.SLEEP_AWAKE_IN_BED || type == HealthDataType.SLEEP_DEEP || + type == HealthDataType.SLEEP_IN_BED || type == HealthDataType.SLEEP_LIGHT || type == HealthDataType.SLEEP_REM || + type == HealthDataType.SLEEP_UNKNOWN || type == HealthDataType.SLEEP_OUT_OF_BED) { value = _convertMinutes(); } @@ -107,6 +119,10 @@ class HealthDataPoint { ElectrocardiogramHealthValue.fromHealthDataPoint(dataPoint), HealthDataType.NUTRITION => NutritionHealthValue.fromHealthDataPoint(dataPoint), + HealthDataType.INSULIN_DELIVERY => + InsulinDeliveryHealthValue.fromHealthDataPoint(dataPoint), + HealthDataType.MENSTRUATION_FLOW => + MenstruationFlowHealthValue.fromHealthDataPoint(dataPoint), _ => NumericHealthValue.fromHealthDataPoint(dataPoint), }; @@ -116,8 +132,11 @@ class HealthDataPoint { DateTime.fromMillisecondsSinceEpoch(dataPoint['date_to'] as int); final String sourceId = dataPoint["source_id"] as String; final String sourceName = dataPoint["source_name"] as String; - final bool isManualEntry = dataPoint["is_manual_entry"] as bool? ?? false; + final Map? metadata = dataPoint["metadata"] == null + ? null + : Map.from(dataPoint['metadata'] as Map); final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT; + final String? uuid = dataPoint["uuid"] as String?; // Set WorkoutSummary, if available. WorkoutSummary? workoutSummary; @@ -128,7 +147,10 @@ class HealthDataPoint { workoutSummary = WorkoutSummary.fromHealthDataPoint(dataPoint); } + var recordingMethod = dataPoint["recording_method"] as int?; + return HealthDataPoint( + uuid: uuid ?? "", value: value, type: dataType, unit: unit, @@ -138,13 +160,15 @@ class HealthDataPoint { sourceDeviceId: Health().deviceId, sourceId: sourceId, sourceName: sourceName, - isManualEntry: isManualEntry, + recordingMethod: RecordingMethod.fromInt(recordingMethod), workoutSummary: workoutSummary, + metadata: metadata, ); } @override String toString() => """$runtimeType - + uuid: $uuid, value: ${value.toString()}, unit: ${unit.name}, dateFrom: $dateFrom, @@ -154,12 +178,14 @@ class HealthDataPoint { deviceId: $sourceDeviceId, sourceId: $sourceId, sourceName: $sourceName - isManualEntry: $isManualEntry - workoutSummary: $workoutSummary"""; + recordingMethod: $recordingMethod + workoutSummary: $workoutSummary + metadata: $metadata"""; @override bool operator ==(Object other) => other is HealthDataPoint && + uuid == other.uuid && value == other.value && unit == other.unit && dateFrom == other.dateFrom && @@ -169,9 +195,10 @@ class HealthDataPoint { sourceDeviceId == other.sourceDeviceId && sourceId == other.sourceId && sourceName == other.sourceName && - isManualEntry == other.isManualEntry; + recordingMethod == other.recordingMethod && + metadata == other.metadata; @override - int get hashCode => Object.hash(value, unit, dateFrom, dateTo, type, - sourcePlatform, sourceDeviceId, sourceId, sourceName); + int get hashCode => Object.hash(uuid, value, unit, dateFrom, dateTo, type, + sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata); } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index cb755553c..cc62e3ac7 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1,8 +1,12 @@ part of '../health.dart'; -/// Main class for the Plugin. This class works as a singleton and should be accessed -/// via `Health()` factory method. The plugin must be configured using the [configure] method -/// before used. +/// Main class for the Plugin. +/// +/// Use this class to get an instance of the Health plugin, like this: +/// +/// final health = Health(); +/// +/// The plugin must be configured using the [configure] method before used. /// /// Overall, the plugin supports: /// @@ -19,29 +23,40 @@ part of '../health.dart'; /// and [getHealthAggregateDataFromTypes] methods. /// * Reading total step counts using the [getTotalStepsInInterval] method. /// * Writing different types of specialized health data like the [writeWorkoutData], -/// [writeBloodPressure], [writeBloodOxygen], [writeAudiogram], and [writeMeal] -/// methods. +/// [writeBloodPressure], [writeBloodOxygen], [writeAudiogram], [writeMeal], +/// [writeMenstruationFlow], [writeInsulinDelivery] methods. +/// +/// On **Android**, this plugin relies on the Google Health Connect (GHC) SDK. +/// Since Health Connect is not installed on SDK level < 34, the plugin has a +/// set of specialized methods to handle GHC: +/// +/// * [getHealthConnectSdkStatus] to check the status of GHC +/// * [isHealthConnectAvailable] to check if GHC is installed on this phone +/// * [installHealthConnect] to direct the user to the app store to install GHC +/// +/// **Note** that you should check the availability of GHC before using any setter +/// or getter methods. Otherwise, the plugin will throw an exception. class Health { static const MethodChannel _channel = MethodChannel('flutter_health'); - static final _instance = Health._(); String? _deviceId; - final _deviceInfo = DeviceInfoPlugin(); - bool _useHealthConnectIfAvailable = false; + final DeviceInfoPlugin _deviceInfo; + HealthConnectSdkStatus _healthConnectSdkStatus = + HealthConnectSdkStatus.sdkUnavailable; - Health._() { + /// Get an instance of the health plugin. + Health({DeviceInfoPlugin? deviceInfo}) + : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { _registerFromJsonFunctions(); } - /// Get the singleton [Health] instance. - factory Health() => _instance; + /// The latest status on availability of Health Connect SDK on this phone. + HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus; /// The type of platform of this device. HealthPlatformType get platformType => Platform.isIOS ? HealthPlatformType.appleHealth - : useHealthConnectIfAvailable - ? HealthPlatformType.googleHealthConnect - : HealthPlatformType.googleFit; + : HealthPlatformType.googleHealthConnect; /// The id of this device. /// @@ -50,25 +65,12 @@ class Health { String get deviceId => _deviceId ?? 'unknown'; /// Configure the health plugin. Must be called before using the plugin. - /// - /// If [useHealthConnectIfAvailable] is true, Google Health Connect on - /// Android will be used. Has no effect on iOS. - Future configure({bool useHealthConnectIfAvailable = false}) async { - _deviceId ??= Platform.isAndroid + Future configure() async { + _deviceId = Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; - - _useHealthConnectIfAvailable = useHealthConnectIfAvailable; - if (_useHealthConnectIfAvailable) { - await _channel.invokeMethod('useHealthConnectIfAvailable'); - } } - /// Is this plugin using Health Connect (true) or Google Fit (false)? - /// - /// This is set in the [configure] method. - bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; - /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid ? dataTypeKeysAndroid.contains(dataType) @@ -102,6 +104,7 @@ class Health { List types, { List? permissions, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( "The lists of types and permissions must be of same length."); @@ -122,85 +125,79 @@ class Health { }); } - /// Revokes permissions of all types. - /// - /// Uses `disableFit()` on Google Fit. + /// Revokes Google Health Connect permissions on Android of all types. /// + /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. + /// + /// Android only. On iOS this does nothing. Future revokePermissions() async { + if (Platform.isIOS) return; + + await _checkIfHealthConnectAvailableOnAndroid(); try { - if (Platform.isIOS) { - throw UnsupportedError( - 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); - } await _channel.invokeMethod('revokePermissions'); - return; } catch (e) { debugPrint('$runtimeType - Exception in revokePermissions(): $e'); } } - /// Returns the current status of Health Connect availability. + /// Checks the current status of Health Connect availability. /// /// See this for more info: /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#getSdkStatus(android.content.Context,kotlin.String) /// - /// Android only. + /// Android only. Returns null on iOS or if an error occurs. Future getHealthConnectSdkStatus() async { + if (Platform.isIOS) return null; + try { - if (Platform.isIOS) { - throw UnsupportedError('Health Connect is not available on iOS.'); - } - final int status = - (await _channel.invokeMethod('getHealthConnectSdkStatus'))!; - return HealthConnectSdkStatus.fromNativeValue(status); + final status = + await _channel.invokeMethod('getHealthConnectSdkStatus'); + _healthConnectSdkStatus = status != null + ? HealthConnectSdkStatus.fromNativeValue(status) + : HealthConnectSdkStatus.sdkUnavailable; + + return _healthConnectSdkStatus; } catch (e) { debugPrint('$runtimeType - Exception in getHealthConnectSdkStatus(): $e'); return null; } } - /// Prompt the user to install the Health Connect app via the installed store - /// (most likely Play Store). + /// Is Google Health Connect available on this phone? /// - /// Android only. + /// Android only. Returns always true on iOS. + Future isHealthConnectAvailable() async => !Platform.isAndroid + ? true + : (await getHealthConnectSdkStatus() == + HealthConnectSdkStatus.sdkAvailable); + + /// Prompt the user to install the Google Health Connect app via the + /// installed store (most likely Play Store). + /// + /// Android only. On iOS this does nothing. Future installHealthConnect() async { + if (Platform.isIOS) return; + try { - if (!Platform.isAndroid) { - throw UnsupportedError( - 'installHealthConnect is only available on Android'); - } await _channel.invokeMethod('installHealthConnect'); } catch (e) { debugPrint('$runtimeType - Exception in installHealthConnect(): $e'); } } - /// Disconnect from Google fit. - /// - /// Not supported on iOS and Google Health Connect, and the method does nothing. - Future disconnect( - List types, { - List? permissions, - }) async { - if (permissions != null && permissions.length != types.length) { - throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); - } - - final mTypes = List.from(types, growable: true); - final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) - : permissions.map((permission) => permission.index).toList(); + /// Checks if Google Health Connect is available and throws an [UnsupportedError] + /// if not. + /// Internal methods used to check availability before any getter or setter methods. + Future _checkIfHealthConnectAvailableOnAndroid() async { + if (!Platform.isAndroid) return; - // on Android, if BMI is requested, then also ask for weight and height - if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); - - List keys = mTypes.map((dataType) => dataType.name).toList(); - - return await _channel.invokeMethod( - 'disconnect', {'types': keys, "permissions": mPermissions}); + if (!(await isHealthConnectAvailable())) { + throw UnsupportedError( + "Google Health Connect is not available on this Android device. " + "You may prompt the user to install it using the 'installHealthConnect' method"); + } } /// Requests permissions to access health data [types]. @@ -227,6 +224,7 @@ class Health { List types, { List? permissions, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( 'The length of [types] must be same as that of [permissions].'); @@ -240,10 +238,11 @@ class Health { type == HealthDataType.HIGH_HEART_RATE_EVENT || type == HealthDataType.LOW_HEART_RATE_EVENT || type == HealthDataType.IRREGULAR_HEART_RATE_EVENT || - type == HealthDataType.WALKING_HEART_RATE) && + type == HealthDataType.WALKING_HEART_RATE || + type == HealthDataType.ATRIAL_FIBRILLATION_BURDEN) && permission != HealthDataAccess.READ) { throw ArgumentError( - 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE is not allowed.'); + 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE / ATRIAL_FIBRILLATION_BURDEN is not allowed.'); } } } @@ -285,17 +284,17 @@ class Health { Future> _computeAndroidBMI( DateTime startTime, DateTime endTime, - bool includeManualEntry, + List recordingMethodsToFilter, ) async { List heights = await _prepareQuery( - startTime, endTime, HealthDataType.HEIGHT, includeManualEntry); + startTime, endTime, HealthDataType.HEIGHT, recordingMethodsToFilter); if (heights.isEmpty) { return []; } List weights = await _prepareQuery( - startTime, endTime, HealthDataType.WEIGHT, includeManualEntry); + startTime, endTime, HealthDataType.WEIGHT, recordingMethodsToFilter); double h = (heights.last.value as NumericHealthValue).numericValue.toDouble(); @@ -309,6 +308,7 @@ class Health { (weights[i].value as NumericHealthValue).numericValue.toDouble() / (h * h); final x = HealthDataPoint( + uuid: '', value: NumericHealthValue(numericValue: bmiValue), type: dataType, unit: unit, @@ -318,7 +318,7 @@ class Health { sourceDeviceId: _deviceId!, sourceId: '', sourceName: '', - isManualEntry: !includeManualEntry, + recordingMethod: RecordingMethod.unknown, ); bmiHealthPoints.add(x); @@ -340,6 +340,8 @@ class Health { /// It must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the [value] is measured /// only at a specific point in time (default). + /// * [recordingMethod] - the recording method of the data point, automatic by default. + /// (on iOS this must be manual or automatic) /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. @@ -349,11 +351,23 @@ class Health { required HealthDataType type, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + if (type == HealthDataType.WORKOUT) { throw ArgumentError( "Adding workouts should be done using the writeWorkoutData method."); } + // If not implemented on platform, throw an exception + if (!isDataTypeAvailable(type)) { + throw HealthException(type, 'Not available on platform $platformType'); + } endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -379,9 +393,7 @@ class Health { type == HealthDataType.SLEEP_IN_BED || type == HealthDataType.SLEEP_DEEP || type == HealthDataType.SLEEP_REM || - type == HealthDataType.SLEEP_ASLEEP_CORE || - type == HealthDataType.SLEEP_ASLEEP_DEEP || - type == HealthDataType.SLEEP_ASLEEP_REM || + type == HealthDataType.SLEEP_LIGHT || type == HealthDataType.HEADACHE_NOT_PRESENT || type == HealthDataType.HEADACHE_MILD || type == HealthDataType.HEADACHE_MODERATE || @@ -395,7 +407,8 @@ class Health { 'dataTypeKey': type.name, 'dataUnitKey': unit.name, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, + 'recordingMethod': recordingMethod.toInt(), }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -416,6 +429,7 @@ class Health { required DateTime startTime, DateTime? endTime, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -443,12 +457,21 @@ class Health { /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood pressure is measured /// only at a specific point in time. If omitted, [endTime] is set to [startTime]. + /// * [recordingMethod] - the recording method of the data point. Future writeBloodPressure({ required int systolic, required int diastolic, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -458,7 +481,8 @@ class Health { 'systolic': systolic, 'diastolic': diastolic, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, + 'recordingMethod': recordingMethod.toInt(), }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -469,20 +493,26 @@ class Health { /// /// Parameters: /// * [saturation] - the saturation of the blood oxygen in percentage - /// * [flowRate] - optional supplemental oxygen flow rate, only supported on - /// Google Fit (default 0.0) /// * [startTime] - the start time when this [saturation] is measured. /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [saturation] is measured. /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood oxygen saturation /// is measured only at a specific point in time (default). + /// * [recordingMethod] - the recording method of the data point. Future writeBloodOxygen({ required double saturation, - double flowRate = 0.0, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -494,21 +524,22 @@ class Health { value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, - endTime: endTime); + endTime: endTime, + recordingMethod: recordingMethod); } else if (Platform.isAndroid) { Map args = { 'value': saturation, - 'flowRate': flowRate, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, + 'recordingMethod': recordingMethod.toInt(), }; success = await _channel.invokeMethod('writeBloodOxygen', args); } return success ?? false; } - /// Saves meal record into Apple Health or Google Fit / Health Connect. + /// Saves meal record into Apple Health or Health Connect. /// /// Returns true if successful, false otherwise. /// @@ -518,11 +549,49 @@ class Health { /// It must be equal to or earlier than [endTime]. /// * [endTime] - the end time when the meal was consumed. /// It must be equal to or later than [startTime]. + /// * [name] - optional name information about this meal. /// * [caloriesConsumed] - total calories consumed with this meal. /// * [carbohydrates] - optional carbohydrates information. /// * [protein] - optional protein information. /// * [fatTotal] - optional total fat information. - /// * [name] - optional name information about this meal. + /// * [caffeine] - optional caffeine information. + /// * [vitaminA] - optional vitamin A information. + /// * [b1Thiamin] - optional vitamin B1 (thiamin) information. + /// * [b2Riboflavin] - optional vitamin B2 (riboflavin) information. + /// * [b3Niacin] - optional vitamin B3 (niacin) information. + /// * [b5PantothenicAcid] - optional vitamin B5 (pantothenic acid) information. + /// * [b6Pyridoxine] - optional vitamin B6 (pyridoxine) information. + /// * [b7Biotin] - optional vitamin B7 (biotin) information. + /// * [b9Folate] - optional vitamin B9 (folate) information. + /// * [b12Cobalamin] - optional vitamin B12 (cobalamin) information. + /// * [vitaminC] - optional vitamin C information. + /// * [vitaminD] - optional vitamin D information. + /// * [vitaminE] - optional vitamin E information. + /// * [vitaminK] - optional vitamin K information. + /// * [calcium] - optional calcium information. + /// * [cholesterol] - optional cholesterol information. + /// * [chloride] - optional chloride information. + /// * [chromium] - optional chromium information. + /// * [copper] - optional copper information. + /// * [fatUnsaturated] - optional unsaturated fat information. + /// * [fatMonounsaturated] - optional monounsaturated fat information. + /// * [fatPolyunsaturated] - optional polyunsaturated fat information. + /// * [fatSaturated] - optional saturated fat information. + /// * [fatTransMonoenoic] - optional trans-monoenoic fat information. + /// * [fiber] - optional fiber information. + /// * [iodine] - optional iodine information. + /// * [iron] - optional iron information. + /// * [magnesium] - optional magnesium information. + /// * [manganese] - optional manganese information. + /// * [molybdenum] - optional molybdenum information. + /// * [phosphorus] - optional phosphorus information. + /// * [potassium] - optional potassium information. + /// * [selenium] - optional selenium information. + /// * [sodium] - optional sodium information. + /// * [sugar] - optional sugar information. + /// * [water] - optional water information. + /// * [zinc] - optional zinc information. + /// * [recordingMethod] - the recording method of the data point. Future writeMeal({ required MealType mealType, required DateTime startTime, @@ -533,26 +602,151 @@ class Health { double? fatTotal, String? name, double? caffeine, + double? vitaminA, + double? b1Thiamin, + double? b2Riboflavin, + double? b3Niacin, + double? b5PantothenicAcid, + double? b6Pyridoxine, + double? b7Biotin, + double? b9Folate, + double? b12Cobalamin, + double? vitaminC, + double? vitaminD, + double? vitaminE, + double? vitaminK, + double? calcium, + double? cholesterol, + double? chloride, + double? chromium, + double? copper, + double? fatUnsaturated, + double? fatMonounsaturated, + double? fatPolyunsaturated, + double? fatSaturated, + double? fatTransMonoenoic, + double? fiber, + double? iodine, + double? iron, + double? magnesium, + double? manganese, + double? molybdenum, + double? phosphorus, + double? potassium, + double? selenium, + double? sodium, + double? sugar, + double? water, + double? zinc, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } Map args = { - 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch, - 'caloriesConsumed': caloriesConsumed, - 'carbohydrates': carbohydrates, - 'protein': protein, - 'fatTotal': fatTotal, 'name': name, + 'meal_type': mealType.name, + 'start_time': startTime.millisecondsSinceEpoch, + 'end_time': endTime.millisecondsSinceEpoch, + 'calories': caloriesConsumed, + 'carbs': carbohydrates, + 'protein': protein, + 'fat': fatTotal, 'caffeine': caffeine, - 'mealType': mealType.name, + 'vitamin_a': vitaminA, + 'b1_thiamin': b1Thiamin, + 'b2_riboflavin': b2Riboflavin, + 'b3_niacin': b3Niacin, + 'b5_pantothenic_acid': b5PantothenicAcid, + 'b6_pyridoxine': b6Pyridoxine, + 'b7_biotin': b7Biotin, + 'b9_folate': b9Folate, + 'b12_cobalamin': b12Cobalamin, + 'vitamin_c': vitaminC, + 'vitamin_d': vitaminD, + 'vitamin_e': vitaminE, + 'vitamin_k': vitaminK, + 'calcium': calcium, + 'cholesterol': cholesterol, + 'chloride': chloride, + 'chromium': chromium, + 'copper': copper, + 'fat_unsaturated': fatUnsaturated, + 'fat_monounsaturated': fatMonounsaturated, + 'fat_polyunsaturated': fatPolyunsaturated, + 'fat_saturated': fatSaturated, + 'fat_trans_monoenoic': fatTransMonoenoic, + 'fiber': fiber, + 'iodine': iodine, + 'iron': iron, + 'magnesium': magnesium, + 'manganese': manganese, + 'molybdenum': molybdenum, + 'phosphorus': phosphorus, + 'potassium': potassium, + 'selenium': selenium, + 'sodium': sodium, + 'sugar': sugar, + 'water': water, + 'zinc': zinc, + 'recordingMethod': recordingMethod.toInt(), }; bool? success = await _channel.invokeMethod('writeMeal', args); return success ?? false; } + /// Save menstruation flow into Apple Health and Google Health Connect. + /// + /// Returns true if successful, false otherwise. + /// + /// Parameters: + /// * [flow] - the menstrual flow + /// * [startTime] - the start time when the menstrual flow is measured. + /// * [endTime] - the start time when the menstrual flow is measured. + /// * [isStartOfCycle] - A bool that indicates whether the sample represents + /// the start of a menstrual cycle. + /// * [recordingMethod] - the recording method of the data point. + Future writeMenstruationFlow({ + required MenstrualFlow flow, + required DateTime startTime, + required DateTime endTime, + required bool isStartOfCycle, + RecordingMethod recordingMethod = RecordingMethod.automatic, + }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + + var value = + Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index; + + if (value == -1) { + throw ArgumentError( + "$flow is not a valid menstrual flow value for $platformType"); + } + + Map args = { + 'value': value, + 'startTime': startTime.millisecondsSinceEpoch, + 'endTime': endTime.millisecondsSinceEpoch, + 'isStartOfCycle': isStartOfCycle, + 'dataTypeKey': HealthDataType.MENSTRUATION_FLOW.name, + 'recordingMethod': recordingMethod.toInt(), + }; + return await _channel.invokeMethod('writeMenstruationFlow', args) == true; + } + /// Saves audiogram into Apple Health. Not supported on Android. /// /// Returns true if successful, false otherwise. @@ -608,18 +802,62 @@ class Health { return await _channel.invokeMethod('writeAudiogram', args) == true; } + /// Saves insulin delivery record into Apple Health. + /// + /// Returns true if successful, false otherwise. + /// + /// Parameters: + /// * [units] - the number of units of insulin taken. + /// * [reason] - the insulin reason, basal or bolus. + /// * [startTime] - the start time when the meal was consumed. + /// It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when the meal was consumed. + /// It must be equal to or later than [startTime]. + Future writeInsulinDelivery( + double units, + InsulinDeliveryReason reason, + DateTime startTime, + DateTime endTime, + ) async { + if (startTime.isAfter(endTime)) { + throw ArgumentError("startTime must be equal or earlier than endTime"); + } + + if (reason == InsulinDeliveryReason.NOT_SET) { + throw ArgumentError("set a valid insulin delivery reason"); + } + + if (Platform.isAndroid) { + throw UnsupportedError( + "writeInsulinDelivery is not supported on Android"); + } + + Map args = { + 'units': units, + 'reason': reason.index, + 'startTime': startTime.millisecondsSinceEpoch, + 'endTime': endTime.millisecondsSinceEpoch + }; + + bool? success = await _channel.invokeMethod('writeInsulinDelivery', args); + return success ?? false; + } + /// Fetch a list of health data points based on [types]. + /// You can also specify the [recordingMethodsToFilter] to filter the data points. + /// If not specified, all data points will be included. Future> getHealthDataFromTypes({ required List types, required DateTime startTime, required DateTime endTime, - bool includeManualEntry = true, + List recordingMethodsToFilter = const [], }) async { + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { - final result = - await _prepareQuery(startTime, endTime, type, includeManualEntry); + final result = await _prepareQuery( + startTime, endTime, type, recordingMethodsToFilter); dataPoints.addAll(result); } @@ -632,17 +870,20 @@ class Health { } /// Fetch a list of health data points based on [types]. + /// You can also specify the [recordingMethodsToFilter] to filter the data points. + /// If not specified, all data points will be included.Vkk Future> getHealthIntervalDataFromTypes( {required DateTime startDate, required DateTime endDate, required List types, required int interval, - bool includeManualEntry = true}) async { + List recordingMethodsToFilter = const []}) async { + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { final result = await _prepareIntervalQuery( - startDate, endDate, type, interval, includeManualEntry); + startDate, endDate, type, interval, recordingMethodsToFilter); dataPoints.addAll(result); } @@ -657,6 +898,7 @@ class Health { int activitySegmentDuration = 1, bool includeManualEntry = true, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; final result = await _prepareAggregateQuery( @@ -671,7 +913,7 @@ class Health { DateTime startTime, DateTime endTime, HealthDataType dataType, - bool includeManualEntry, + List recordingMethodsToFilter, ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid @@ -686,9 +928,10 @@ class Health { // If BodyMassIndex is requested on Android, calculate this manually if (dataType == HealthDataType.BODY_MASS_INDEX && Platform.isAndroid) { - return _computeAndroidBMI(startTime, endTime, includeManualEntry); + return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } - return await _dataQuery(startTime, endTime, dataType, includeManualEntry); + return await _dataQuery( + startTime, endTime, dataType, recordingMethodsToFilter); } /// Prepares an interval query, i.e. checks if the types are available, etc. @@ -697,7 +940,7 @@ class Health { DateTime endDate, HealthDataType dataType, int interval, - bool includeManualEntry) async { + List recordingMethodsToFilter) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -710,7 +953,7 @@ class Health { } return await _dataIntervalQuery( - startDate, endDate, dataType, interval, includeManualEntry); + startDate, endDate, dataType, interval, recordingMethodsToFilter); } /// Prepares an aggregate query, i.e. checks if the types are available, etc. @@ -737,14 +980,18 @@ class Health { } /// Fetches data points from Android/iOS native code. - Future> _dataQuery(DateTime startTime, DateTime endTime, - HealthDataType dataType, bool includeManualEntry) async { + Future> _dataQuery( + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'includeManualEntry': includeManualEntry + 'recordingMethodsToFilter': + recordingMethodsToFilter.map((e) => e.toInt()).toList(), }; final fetchedDataPoints = await _channel.invokeMethod('getData', args); @@ -771,14 +1018,15 @@ class Health { DateTime endDate, HealthDataType dataType, int interval, - bool includeManualEntry) async { + List recordingMethodsToFilter) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'interval': interval, - 'includeManualEntry': includeManualEntry + 'recordingMethodsToFilter': + recordingMethodsToFilter.map((e) => e.toInt()).toList(), }; final fetchedDataPoints = @@ -837,15 +1085,14 @@ class Health { /// Get the total number of steps within a specific time period. /// Returns null if not successful. - /// - /// Is a fix according to https://stackoverflow.com/questions/29414386/step-count-retrieved-through-google-fit-api-does-not-match-step-count-displayed/29415091#29415091 - Future getTotalStepsInInterval( - DateTime startTime, - DateTime endTime, - ) async { + Future getTotalStepsInInterval(DateTime startTime, DateTime endTime, + {bool includeManualEntry = true}) async { final args = { 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, + 'recordingMethodsToFilter': includeManualEntry + ? [] + : [RecordingMethod.manual.toInt()], }; final stepsCount = await _channel.invokeMethod( 'getTotalStepsInInterval', @@ -857,13 +1104,11 @@ class Health { /// Assigns numbers to specific [HealthDataType]s. int _alignValue(HealthDataType type) => switch (type) { HealthDataType.SLEEP_IN_BED => 0, + HealthDataType.SLEEP_ASLEEP => 1, HealthDataType.SLEEP_AWAKE => 2, - HealthDataType.SLEEP_ASLEEP => 3, + HealthDataType.SLEEP_LIGHT => 3, HealthDataType.SLEEP_DEEP => 4, HealthDataType.SLEEP_REM => 5, - HealthDataType.SLEEP_ASLEEP_CORE => 3, - HealthDataType.SLEEP_ASLEEP_DEEP => 4, - HealthDataType.SLEEP_ASLEEP_REM => 5, HealthDataType.HEADACHE_UNSPECIFIED => 0, HealthDataType.HEADACHE_NOT_PRESENT => 1, HealthDataType.HEADACHE_MILD => 2, @@ -873,7 +1118,7 @@ class Health { "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues"), }; - /// Write workout data to Apple Health or Google Fit or Google Health Connect. + /// Write workout data to Apple Health or Google Health Connect. /// /// Returns true if the workout data was successfully added. /// @@ -889,6 +1134,7 @@ class Health { /// *ONLY FOR IOS* Default value is METER. /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". + /// - [recordingMethod] The recording method of the data point, automatic by default (on iOS this can only be automatic or manual). Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, @@ -898,7 +1144,15 @@ class Health { int? totalDistance, HealthDataUnit totalDistanceUnit = HealthDataUnit.METER, String? title, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailableOnAndroid(); + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + // Check that value is on the current Platform if (Platform.isIOS && !_isOnIOS(activityType)) { throw HealthException(activityType, @@ -916,6 +1170,7 @@ class Health { 'totalDistance': totalDistance, 'totalDistanceUnit': totalDistanceUnit.name, 'title': title, + 'recordingMethod': recordingMethod.toInt(), }; return await _channel.invokeMethod('writeWorkoutData', args) == true; } @@ -924,84 +1179,87 @@ class Health { bool _isOnIOS(HealthWorkoutActivityType type) { // Returns true if the type is part of the iOS set return { + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, + HealthWorkoutActivityType.BARRE, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, + HealthWorkoutActivityType.BOWLING, HealthWorkoutActivityType.BOXING, + HealthWorkoutActivityType.CARDIO_DANCE, + HealthWorkoutActivityType.CLIMBING, + HealthWorkoutActivityType.COOLDOWN, + HealthWorkoutActivityType.CORE_TRAINING, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, + HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DISC_SPORTS, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, + HealthWorkoutActivityType.EQUESTRIAN_SPORTS, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.FISHING, + HealthWorkoutActivityType.FITNESS_GAMING, + HealthWorkoutActivityType.FLEXIBILITY, + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, + HealthWorkoutActivityType.HAND_CYCLING, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.HUNTING, HealthWorkoutActivityType.JUMP_ROPE, HealthWorkoutActivityType.KICKBOXING, + HealthWorkoutActivityType.LACROSSE, HealthWorkoutActivityType.MARTIAL_ARTS, + HealthWorkoutActivityType.MIND_AND_BODY, + HealthWorkoutActivityType.MIXED_CARDIO, + HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.PADDLE_SPORTS, + HealthWorkoutActivityType.PICKLEBALL, HealthWorkoutActivityType.PILATES, + HealthWorkoutActivityType.PLAY, + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, HealthWorkoutActivityType.RACQUETBALL, HealthWorkoutActivityType.ROWING, HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.SNOW_SPORTS, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.SOCIAL_DANCE, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, + HealthWorkoutActivityType.STAIRS, + HealthWorkoutActivityType.STEP_TRAINING, + HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, + HealthWorkoutActivityType.TAI_CHI, HealthWorkoutActivityType.TENNIS, - HealthWorkoutActivityType.VOLLEYBALL, - HealthWorkoutActivityType.WALKING, - HealthWorkoutActivityType.WATER_POLO, - HealthWorkoutActivityType.YOGA, - HealthWorkoutActivityType.BOWLING, - HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.TRACK_AND_FIELD, - HealthWorkoutActivityType.DISC_SPORTS, - HealthWorkoutActivityType.LACROSSE, - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, - HealthWorkoutActivityType.FLEXIBILITY, - HealthWorkoutActivityType.COOLDOWN, - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, - HealthWorkoutActivityType.HAND_CYCLING, - HealthWorkoutActivityType.CORE_TRAINING, - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING, - HealthWorkoutActivityType.MIXED_CARDIO, - HealthWorkoutActivityType.STAIRS, - HealthWorkoutActivityType.STEP_TRAINING, - HealthWorkoutActivityType.FITNESS_GAMING, - HealthWorkoutActivityType.BARRE, - HealthWorkoutActivityType.CARDIO_DANCE, - HealthWorkoutActivityType.SOCIAL_DANCE, - HealthWorkoutActivityType.MIND_AND_BODY, - HealthWorkoutActivityType.PICKLEBALL, - HealthWorkoutActivityType.CLIMBING, - HealthWorkoutActivityType.EQUESTRIAN_SPORTS, - HealthWorkoutActivityType.FISHING, - HealthWorkoutActivityType.HUNTING, - HealthWorkoutActivityType.PLAY, - HealthWorkoutActivityType.SNOW_SPORTS, - HealthWorkoutActivityType.PADDLE_SPORTS, - HealthWorkoutActivityType.SURFING_SPORTS, + HealthWorkoutActivityType.VOLLEYBALL, + HealthWorkoutActivityType.WALKING, HealthWorkoutActivityType.WATER_FITNESS, + HealthWorkoutActivityType.WATER_POLO, HealthWorkoutActivityType.WATER_SPORTS, - HealthWorkoutActivityType.TAI_CHI, + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.WRESTLING, - HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.YOGA, + HealthWorkoutActivityType.SWIMMING_OPEN_WATER, + HealthWorkoutActivityType.SWIMMING_POOL, + HealthWorkoutActivityType.UNDERWATER_DIVING, }.contains(type); } @@ -1010,28 +1268,27 @@ class Health { // Returns true if the type is part of the Android set return { // Both + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, HealthWorkoutActivityType.BOXING, + HealthWorkoutActivityType.CARDIO_DANCE, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, - HealthWorkoutActivityType.JUMP_ROPE, - HealthWorkoutActivityType.KICKBOXING, HealthWorkoutActivityType.MARTIAL_ARTS, HealthWorkoutActivityType.PILATES, HealthWorkoutActivityType.RACQUETBALL, @@ -1039,91 +1296,44 @@ class Health { HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.SOCIAL_DANCE, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, - HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, HealthWorkoutActivityType.TENNIS, HealthWorkoutActivityType.VOLLEYBALL, HealthWorkoutActivityType.WALKING, HealthWorkoutActivityType.WATER_POLO, + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.YOGA, // Android only - // Once Google Fit is removed, this list needs to be changed - HealthWorkoutActivityType.AEROBICS, - HealthWorkoutActivityType.BIATHLON, - HealthWorkoutActivityType.BIKING_HAND, - HealthWorkoutActivityType.BIKING_MOUNTAIN, - HealthWorkoutActivityType.BIKING_ROAD, - HealthWorkoutActivityType.BIKING_SPINNING, HealthWorkoutActivityType.BIKING_STATIONARY, - HealthWorkoutActivityType.BIKING_UTILITY, HealthWorkoutActivityType.CALISTHENICS, - HealthWorkoutActivityType.CIRCUIT_TRAINING, - HealthWorkoutActivityType.CROSS_FIT, HealthWorkoutActivityType.DANCING, - HealthWorkoutActivityType.DIVING, - HealthWorkoutActivityType.ELEVATOR, - HealthWorkoutActivityType.ERGOMETER, - HealthWorkoutActivityType.ESCALATOR, HealthWorkoutActivityType.FRISBEE_DISC, - HealthWorkoutActivityType.GARDENING, HealthWorkoutActivityType.GUIDED_BREATHING, - HealthWorkoutActivityType.HORSEBACK_RIDING, - HealthWorkoutActivityType.HOUSEWORK, - HealthWorkoutActivityType.INTERVAL_TRAINING, - HealthWorkoutActivityType.IN_VEHICLE, HealthWorkoutActivityType.ICE_SKATING, - HealthWorkoutActivityType.KAYAKING, - HealthWorkoutActivityType.KETTLEBELL_TRAINING, - HealthWorkoutActivityType.KICK_SCOOTER, - HealthWorkoutActivityType.KITE_SURFING, - HealthWorkoutActivityType.MEDITATION, - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS, - HealthWorkoutActivityType.P90X, HealthWorkoutActivityType.PARAGLIDING, - HealthWorkoutActivityType.POLO, HealthWorkoutActivityType.ROCK_CLIMBING, HealthWorkoutActivityType.ROWING_MACHINE, - HealthWorkoutActivityType.RUNNING_JOGGING, - HealthWorkoutActivityType.RUNNING_SAND, HealthWorkoutActivityType.RUNNING_TREADMILL, HealthWorkoutActivityType.SCUBA_DIVING, - HealthWorkoutActivityType.SKATING_CROSS, - HealthWorkoutActivityType.SKATING_INDOOR, - HealthWorkoutActivityType.SKATING_INLINE, HealthWorkoutActivityType.SKIING, - HealthWorkoutActivityType.SKIING_BACK_COUNTRY, - HealthWorkoutActivityType.SKIING_KITE, - HealthWorkoutActivityType.SKIING_ROLLER, - HealthWorkoutActivityType.SLEDDING, - HealthWorkoutActivityType.SNOWMOBILE, HealthWorkoutActivityType.SNOWSHOEING, HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE, - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING, - HealthWorkoutActivityType.STILL, HealthWorkoutActivityType.STRENGTH_TRAINING, HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.TEAM_SPORTS, - HealthWorkoutActivityType.TILTING, - HealthWorkoutActivityType.VOLLEYBALL_BEACH, - HealthWorkoutActivityType.VOLLEYBALL_INDOOR, - HealthWorkoutActivityType.WAKEBOARDING, - HealthWorkoutActivityType.WALKING_FITNESS, - HealthWorkoutActivityType.WALKING_NORDIC, - HealthWorkoutActivityType.WALKING_STROLLER, HealthWorkoutActivityType.WALKING_TREADMILL, HealthWorkoutActivityType.WEIGHTLIFTING, HealthWorkoutActivityType.WHEELCHAIR, - HealthWorkoutActivityType.WINDSURFING, - HealthWorkoutActivityType.ZUMBA, HealthWorkoutActivityType.OTHER, }.contains(type); } diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index ad5fdecbb..af13e1892 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -1,24 +1,24 @@ part of '../health.dart'; /// An abstract class for health values. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class HealthValue extends Serializable { HealthValue(); @override Function get fromJsonFunction => _$HealthValueFromJson; factory HealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as HealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$HealthValueToJson(this); } -/// A numerical value from Apple HealthKit or Google Fit +/// A numerical value from Apple HealthKit or Google Health Connect /// such as integer or double. E.g. 1, 2.9, -3 /// /// Parameters: /// * [numericValue] - a [num] value for the [HealthDataPoint] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class NumericHealthValue extends HealthValue { /// A [num] value for the [HealthDataPoint]. num numericValue; @@ -35,7 +35,7 @@ class NumericHealthValue extends HealthValue { @override Function get fromJsonFunction => _$NumericHealthValueFromJson; factory NumericHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as NumericHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$NumericHealthValueToJson(this); @@ -53,7 +53,7 @@ class NumericHealthValue extends HealthValue { /// * [frequencies] - array of frequencies of the test /// * [leftEarSensitivities] threshold in decibel for the left ear /// * [rightEarSensitivities] threshold in decibel for the left ear -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class AudiogramHealthValue extends HealthValue { /// Array of frequencies of the test. List frequencies; @@ -87,7 +87,7 @@ class AudiogramHealthValue extends HealthValue { @override Function get fromJsonFunction => _$AudiogramHealthValueFromJson; factory AudiogramHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as AudiogramHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$AudiogramHealthValueToJson(this); @@ -111,7 +111,7 @@ class AudiogramHealthValue extends HealthValue { /// * [totalEnergyBurnedUnit] - the unit of the total energy burned /// * [totalDistance] - the total distance of the workout /// * [totalDistanceUnit] - the unit of the total distance -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class WorkoutHealthValue extends HealthValue { /// The type of the workout. HealthWorkoutActivityType workoutActivityType; @@ -153,7 +153,9 @@ class WorkoutHealthValue extends HealthValue { factory WorkoutHealthValue.fromHealthDataPoint(dynamic dataPoint) => WorkoutHealthValue( workoutActivityType: HealthWorkoutActivityType.values.firstWhere( - (element) => element.name == dataPoint['workoutActivityType']), + (element) => element.name == dataPoint['workoutActivityType'], + orElse: () => HealthWorkoutActivityType.OTHER, + ), totalEnergyBurned: dataPoint['totalEnergyBurned'] != null ? (dataPoint['totalEnergyBurned'] as num).toInt() : null, @@ -179,7 +181,7 @@ class WorkoutHealthValue extends HealthValue { @override Function get fromJsonFunction => _$WorkoutHealthValueFromJson; factory WorkoutHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as WorkoutHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$WorkoutHealthValueToJson(this); @@ -222,7 +224,7 @@ class WorkoutHealthValue extends HealthValue { /// * [averageHeartRate] - the average heart rate during the ECG (in BPM) /// * [samplingFrequency] - the frequency at which the Apple Watch sampled the voltage. /// * [classification] - an [ElectrocardiogramClassification] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class ElectrocardiogramHealthValue extends HealthValue { /// An array of [ElectrocardiogramVoltageValue]s. List voltageValues; @@ -246,7 +248,7 @@ class ElectrocardiogramHealthValue extends HealthValue { @override Function get fromJsonFunction => _$ElectrocardiogramHealthValueFromJson; factory ElectrocardiogramHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as ElectrocardiogramHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$ElectrocardiogramHealthValueToJson(this); @@ -281,7 +283,7 @@ class ElectrocardiogramHealthValue extends HealthValue { } /// Single voltage value belonging to a [ElectrocardiogramHealthValue] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class ElectrocardiogramVoltageValue extends HealthValue { /// Voltage of the ECG. num voltage; @@ -304,7 +306,7 @@ class ElectrocardiogramVoltageValue extends HealthValue { @override Function get fromJsonFunction => _$ElectrocardiogramVoltageValueFromJson; factory ElectrocardiogramVoltageValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as ElectrocardiogramVoltageValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$ElectrocardiogramVoltageValueToJson(this); @@ -321,77 +323,300 @@ class ElectrocardiogramVoltageValue extends HealthValue { String toString() => '$runtimeType - voltage: $voltage'; } +/// A [HealthValue] object from insulin delivery (iOS only) +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class InsulinDeliveryHealthValue extends HealthValue { + /// The amount of units of insulin taken + double units; + + /// If it's basal, bolus or unknown reason for insulin dosage + InsulinDeliveryReason reason; + + InsulinDeliveryHealthValue({ + required this.units, + required this.reason, + }); + + factory InsulinDeliveryHealthValue.fromHealthDataPoint(dynamic dataPoint) { + final units = dataPoint['value'] as num; + + final metadata = dataPoint['metadata'] == null + ? null + : Map.from(dataPoint['metadata'] as Map); + final reasonIndex = + metadata == null || !metadata.containsKey('HKInsulinDeliveryReason') + ? 0 + : metadata['HKInsulinDeliveryReason'] as double; + final reason = InsulinDeliveryReason.values[reasonIndex.toInt()]; + + return InsulinDeliveryHealthValue(units: units.toDouble(), reason: reason); + } + + @override + Function get fromJsonFunction => _$InsulinDeliveryHealthValueFromJson; + factory InsulinDeliveryHealthValue.fromJson(Map json) => + FromJsonFactory().fromJson(json); + @override + Map toJson() => _$InsulinDeliveryHealthValueToJson(this); + + @override + bool operator ==(Object other) => + other is InsulinDeliveryHealthValue && + units == other.units && + reason == other.reason; + + @override + int get hashCode => Object.hash(units, reason); + + @override + String toString() => '$runtimeType - units: $units, reason: $reason'; +} + /// A [HealthValue] object for nutrition. /// /// Parameters: -/// * [protein] - the amount of protein in grams -/// * [calories] - the amount of calories in kcal -/// * [fat] - the amount of fat in grams +/// * [mealType] - the type of meal /// * [name] - the name of the food -/// * [carbs] - the amount of carbs in grams +/// * [b1Thiamine] - the amount of thiamine (B1) in grams +/// * [b2Riboflavin] - the amount of riboflavin (B2) in grams +/// * [b3Niacin] - the amount of niacin (B3) in grams +/// * [b5PantothenicAcid] - the amount of pantothenic acid (B5) in grams +/// * [b6Pyridoxine] - the amount of pyridoxine (B6) in grams +/// * [b7Biotin] - the amount of biotin (B7) in grams +/// * [b9Folate] - the amount of folate (B9) in grams +/// * [b12Cobalamin] - the amount of cobalamin (B12) in grams /// * [caffeine] - the amount of caffeine in grams -/// * [mealType] - the type of meal -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +/// * [calcium] - the amount of calcium in grams +/// * [calories] - the amount of calories in kcal +/// * [carbs] - the amount of carbs in grams +/// * [chloride] - the amount of chloride in grams +/// * [cholesterol] - the amount of cholesterol in grams +/// * [choline] - the amount of choline in grams +/// * [chromium] - the amount of chromium in grams +/// * [copper] - the amount of copper in grams +/// * [fat] - the amount of fat in grams +/// * [fatMonounsaturated] - the amount of monounsaturated fat in grams +/// * [fatPolyunsaturated] - the amount of polyunsaturated fat in grams +/// * [fatSaturated] - the amount of saturated fat in grams +/// * [fatTransMonoenoic] - the amount of +/// * [fatUnsaturated] - the amount of unsaturated fat in grams +/// * [fiber] - the amount of fiber in grams +/// * [iodine] - the amount of iodine in grams +/// * [iron] - the amount of iron in grams +/// * [magnesium] - the amount of magnesium in grams +/// * [manganese] - the amount of manganese in grams +/// * [molybdenum] - the amount of molybdenum in grams +/// * [phosphorus] - the amount of phosphorus in grams +/// * [potassium] - the amount of potassium in grams +/// * [protein] - the amount of protein in grams +/// * [selenium] - the amount of selenium in grams +/// * [sodium] - the amount of sodium in grams +/// * [sugar] - the amount of sugar in grams +/// * [vitaminA] - the amount of vitamin A in grams +/// * [vitaminC] - the amount of vitamin C in grams +/// * [vitaminD] - the amount of vitamin D in grams +/// * [vitaminE] - the amount of vitamin E in grams +/// * [vitaminK] - the amount of vitamin K in grams +/// * [water] - the amount of water in grams +/// * [zinc] - the amount of zinc in grams + +@JsonSerializable(includeIfNull: false, explicitToJson: true) class NutritionHealthValue extends HealthValue { + /// The name of the food. + String? name; + /// The type of meal. String? mealType; - /// The amount of protein in grams. - double? protein; - /// The amount of calories in kcal. double? calories; + /// The amount of protein in grams. + double? protein; + /// The amount of fat in grams. double? fat; - /// The name of the food. - String? name; - /// The amount of carbs in grams. double? carbs; /// The amount of caffeine in grams. double? caffeine; + /// The amount of vitamin A in grams. + double? vitaminA; + + /// The amount of thiamine (B1) in grams. + double? b1Thiamine; + + /// The amount of riboflavin (B2) in grams. + double? b2Riboflavin; + + /// The amount of niacin (B3) in grams. + double? b3Niacin; + + /// The amount of pantothenic acid (B5) in grams. + double? b5PantothenicAcid; + + /// The amount of pyridoxine (B6) in grams. + double? b6Pyridoxine; + + /// The amount of biotin (B7) in grams. + double? b7Biotin; + + /// The amount of folate (B9) in grams. + double? b9Folate; + + /// The amount of cobalamin (B12) in grams. + double? b12Cobalamin; + + /// The amount of vitamin C in grams. + double? vitaminC; + + /// The amount of vitamin D in grams. + double? vitaminD; + + /// The amount of vitamin E in grams. + double? vitaminE; + + /// The amount of vitamin K in grams. + double? vitaminK; + + /// The amount of calcium in grams. + double? calcium; + + /// The amount of chloride in grams. + double? chloride; + + /// The amount of cholesterol in grams. + double? cholesterol; + + /// The amount of choline in grams. + double? choline; + + /// The amount of chromium in grams. + double? chromium; + + /// The amount of copper in grams. + double? copper; + + /// The amount of unsaturated fat in grams. + double? fatUnsaturated; + + /// The amount of monounsaturated fat in grams. + double? fatMonounsaturated; + + /// The amount of polyunsaturated fat in grams. + double? fatPolyunsaturated; + + /// The amount of saturated fat in grams. + double? fatSaturated; + + /// The amount of trans-monoenoic fat in grams. + double? fatTransMonoenoic; + + /// The amount of fiber in grams. + double? fiber; + + /// The amount of iodine in grams. + double? iodine; + + /// The amount of iron in grams. + double? iron; + + /// The amount of magnesium in grams. + double? magnesium; + + /// The amount of manganese in grams. + double? manganese; + + /// The amount of molybdenum in grams. + double? molybdenum; + + /// The amount of phosphorus in grams. + double? phosphorus; + + /// The amount of potassium in grams. + double? potassium; + + /// The amount of selenium in grams. + double? selenium; + + /// The amount of sodium in grams. + double? sodium; + + /// The amount of sugar in grams. + double? sugar; + + /// The amount of water in grams. + double? water; + + /// The amount of zinc in grams. + double? zinc; + NutritionHealthValue({ + this.name, this.mealType, - this.protein, this.calories, + this.protein, this.fat, - this.name, this.carbs, this.caffeine, + this.vitaminA, + this.b1Thiamine, + this.b2Riboflavin, + this.b3Niacin, + this.b5PantothenicAcid, + this.b6Pyridoxine, + this.b7Biotin, + this.b9Folate, + this.b12Cobalamin, + this.vitaminC, + this.vitaminD, + this.vitaminE, + this.vitaminK, + this.calcium, + this.chloride, + this.cholesterol, + this.choline, + this.chromium, + this.copper, + this.fatUnsaturated, + this.fatMonounsaturated, + this.fatPolyunsaturated, + this.fatSaturated, + this.fatTransMonoenoic, + this.fiber, + this.iodine, + this.iron, + this.magnesium, + this.manganese, + this.molybdenum, + this.phosphorus, + this.potassium, + this.selenium, + this.sodium, + this.sugar, + this.water, + this.zinc, }); @override Function get fromJsonFunction => _$NutritionHealthValueFromJson; factory NutritionHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as NutritionHealthValue; + (json) as NutritionHealthValue; @override Map toJson() => _$NutritionHealthValueToJson(this); /// Create a [NutritionHealthValue] based on a health data point from native data format. - factory NutritionHealthValue.fromHealthDataPoint(dynamic dataPoint) => - NutritionHealthValue( - mealType: dataPoint['mealType'] as String, - protein: dataPoint['protein'] != null - ? (dataPoint['protein'] as num).toDouble() - : null, - calories: dataPoint['calories'] != null - ? (dataPoint['calories'] as num).toDouble() - : null, - fat: dataPoint['fat'] != null - ? (dataPoint['fat'] as num).toDouble() - : null, - name: dataPoint['name'] != null ? (dataPoint['name'] as String) : null, - carbs: dataPoint['carbs'] != null - ? (dataPoint['carbs'] as num).toDouble() - : null, - caffeine: dataPoint['caffeine'] != null - ? (dataPoint['caffeine'] as num).toDouble() - : null, - ); + factory NutritionHealthValue.fromHealthDataPoint(dynamic dataPoint) { + dataPoint = dataPoint as Map; + // where key is not null + final Map dataPointMap = Map.fromEntries(dataPoint.entries + .where((entry) => entry.key != null) + .map((entry) => MapEntry(entry.key as String, entry.value))); + return _$NutritionHealthValueFromJson(dataPointMap); + } @override String toString() => """$runtimeType - protein: ${protein.toString()}, @@ -400,20 +625,308 @@ class NutritionHealthValue extends HealthValue { name: ${name.toString()}, carbs: ${carbs.toString()}, caffeine: ${caffeine.toString()}, - mealType: $mealType"""; + mealType: $mealType, + vitaminA: ${vitaminA.toString()}, + b1Thiamine: ${b1Thiamine.toString()}, + b2Riboflavin: ${b2Riboflavin.toString()}, + b3Niacin: ${b3Niacin.toString()}, + b5PantothenicAcid: ${b5PantothenicAcid.toString()}, + b6Pyridoxine: ${b6Pyridoxine.toString()}, + b7Biotin: ${b7Biotin.toString()}, + b9Folate: ${b9Folate.toString()}, + b12Cobalamin: ${b12Cobalamin.toString()}, + vitaminC: ${vitaminC.toString()}, + vitaminD: ${vitaminD.toString()}, + vitaminE: ${vitaminE.toString()}, + vitaminK: ${vitaminK.toString()}, + calcium: ${calcium.toString()}, + chloride: ${chloride.toString()}, + cholesterol: ${cholesterol.toString()}, + choline: ${choline.toString()}, + chromium: ${chromium.toString()}, + copper: ${copper.toString()}, + unsaturatedFat: ${fatUnsaturated.toString()}, + fatMonounsaturated: ${fatMonounsaturated.toString()}, + fatPolyunsaturated: ${fatPolyunsaturated.toString()}, + fatSaturated: ${fatSaturated.toString()}, + fatTransMonoenoic: ${fatTransMonoenoic.toString()}, + fiber: ${fiber.toString()}, + iodine: ${iodine.toString()}, + iron: ${iron.toString()}, + magnesium: ${magnesium.toString()}, + manganese: ${manganese.toString()}, + molybdenum: ${molybdenum.toString()}, + phosphorus: ${phosphorus.toString()}, + potassium: ${potassium.toString()}, + selenium: ${selenium.toString()}, + sodium: ${sodium.toString()}, + sugar: ${sugar.toString()}, + water: ${water.toString()}, + zinc: ${zinc.toString()}"""; @override bool operator ==(Object other) => other is NutritionHealthValue && - other.protein == protein && + other.name == name && + other.mealType == mealType && other.calories == calories && + other.protein == protein && other.fat == fat && - other.name == name && other.carbs == carbs && other.caffeine == caffeine && - other.mealType == mealType; + other.vitaminA == vitaminA && + other.b1Thiamine == b1Thiamine && + other.b2Riboflavin == b2Riboflavin && + other.b3Niacin == b3Niacin && + other.b5PantothenicAcid == b5PantothenicAcid && + other.b6Pyridoxine == b6Pyridoxine && + other.b7Biotin == b7Biotin && + other.b9Folate == b9Folate && + other.b12Cobalamin == b12Cobalamin && + other.vitaminC == vitaminC && + other.vitaminD == vitaminD && + other.vitaminE == vitaminE && + other.vitaminK == vitaminK && + other.calcium == calcium && + other.chloride == chloride && + other.cholesterol == cholesterol && + other.choline == choline && + other.chromium == chromium && + other.copper == copper && + other.fatUnsaturated == fatUnsaturated && + other.fatMonounsaturated == fatMonounsaturated && + other.fatPolyunsaturated == fatPolyunsaturated && + other.fatSaturated == fatSaturated && + other.fatTransMonoenoic == fatTransMonoenoic && + other.fiber == fiber && + other.iodine == iodine && + other.iron == iron && + other.magnesium == magnesium && + other.manganese == manganese && + other.molybdenum == molybdenum && + other.phosphorus == phosphorus && + other.potassium == potassium && + other.selenium == selenium && + other.sodium == sodium && + other.sugar == sugar && + other.water == water && + other.zinc == zinc; + + @override + int get hashCode => Object.hashAll([ + protein, + calories, + fat, + name, + carbs, + caffeine, + vitaminA, + b1Thiamine, + b2Riboflavin, + b3Niacin, + b5PantothenicAcid, + b6Pyridoxine, + b7Biotin, + b9Folate, + b12Cobalamin, + vitaminC, + vitaminD, + vitaminE, + vitaminK, + calcium, + chloride, + cholesterol, + choline, + chromium, + copper, + fatUnsaturated, + fatMonounsaturated, + fatPolyunsaturated, + fatSaturated, + fatTransMonoenoic, + fiber, + iodine, + iron, + magnesium, + manganese, + molybdenum, + phosphorus, + potassium, + selenium, + sodium, + sugar, + water, + zinc, + ]); +} + +enum MenstrualFlow { + unspecified, + none, + light, + medium, + heavy, + spotting; + + static MenstrualFlow fromHealthConnect(int value) { + switch (value) { + case 0: + return MenstrualFlow.unspecified; + case 1: + return MenstrualFlow.light; + case 2: + return MenstrualFlow.medium; + case 3: + return MenstrualFlow.heavy; + default: + return MenstrualFlow.unspecified; + } + } + + static MenstrualFlow fromHealthKit(int value) { + switch (value) { + case 1: + return MenstrualFlow.unspecified; + case 2: + return MenstrualFlow.light; + case 3: + return MenstrualFlow.medium; + case 4: + return MenstrualFlow.heavy; + case 5: + return MenstrualFlow.none; + default: + return MenstrualFlow.unspecified; + } + } + + static int toHealthConnect(MenstrualFlow value) { + switch (value) { + case MenstrualFlow.unspecified: + return 0; + case MenstrualFlow.light: + return 1; + case MenstrualFlow.medium: + return 2; + case MenstrualFlow.heavy: + return 3; + default: + return -1; + } + } +} + +enum RecordingMethod { + unknown, + active, + automatic, + manual; + + /// Create a [RecordingMethod] from an integer. + /// 0: unknown, 1: active, 2: automatic, 3: manual + /// If the integer is not in the range of 0-3, [RecordingMethod.unknown] is returned. + /// This is used to align the recording method with the platform. + static RecordingMethod fromInt(int? recordingMethod) { + switch (recordingMethod) { + case 0: + return RecordingMethod.unknown; + case 1: + return RecordingMethod.active; + case 2: + return RecordingMethod.automatic; + case 3: + return RecordingMethod.manual; + default: + return RecordingMethod.unknown; + } + } + + /// Convert this [RecordingMethod] to an integer. + int toInt() { + switch (this) { + case RecordingMethod.unknown: + return 0; + case RecordingMethod.active: + return 1; + case RecordingMethod.automatic: + return 2; + case RecordingMethod.manual: + return 3; + } + } +} + +/// A [HealthValue] object for menstrual flow. +/// +/// Parameters: +/// * [flowValue] - the flow value +/// * [isStartOfCycle] - indicator whether or not this occurrence is the first day of the menstrual cycle (iOS only) +/// * [wasUserEntered] - indicator whether or not the data was entered by the user (iOS only) +/// * [dateTime] - the date and time of the menstrual flow +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class MenstruationFlowHealthValue extends HealthValue { + final MenstrualFlow? flow; + final bool? isStartOfCycle; + final bool? wasUserEntered; + final DateTime dateTime; + + MenstruationFlowHealthValue({ + required this.flow, + required this.dateTime, + this.isStartOfCycle, + this.wasUserEntered, + }); + + @override + String toString() => + "flow: ${flow?.name}, startOfCycle: $isStartOfCycle, wasUserEntered: $wasUserEntered, dateTime: $dateTime"; + + factory MenstruationFlowHealthValue.fromHealthDataPoint(dynamic dataPoint) { + // Parse flow value safely + final flowValueIndex = dataPoint['value'] as int? ?? 0; + MenstrualFlow? menstrualFlow; + if (Platform.isAndroid) { + menstrualFlow = MenstrualFlow.fromHealthConnect(flowValueIndex); + } else if (Platform.isIOS) { + menstrualFlow = MenstrualFlow.fromHealthKit(flowValueIndex); + } + + return MenstruationFlowHealthValue( + flow: menstrualFlow, + isStartOfCycle: + dataPoint['metadata']?.containsKey('HKMenstrualCycleStart') == true + ? dataPoint['metadata']['HKMenstrualCycleStart'] == 1.0 + : null, + wasUserEntered: + dataPoint['metadata']?.containsKey('HKWasUserEntered') == true + ? dataPoint['metadata']['HKWasUserEntered'] == 1.0 + : null, + dateTime: + DateTime.fromMillisecondsSinceEpoch(dataPoint['date_from'] as int), + ); + } + + @override + Function get fromJsonFunction => _$MenstruationFlowHealthValueFromJson; + + factory MenstruationFlowHealthValue.fromJson(Map json) => + FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$MenstruationFlowHealthValueToJson(this); + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is MenstruationFlowHealthValue && + runtimeType == other.runtimeType && + flow == other.flow && + isStartOfCycle == other.isStartOfCycle && + wasUserEntered == other.wasUserEntered && + dateTime == other.dateTime; + } @override int get hashCode => - Object.hash(protein, calories, fat, name, carbs, caffeine); + Object.hash(flow, isStartOfCycle, wasUserEntered, dateTime); } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 7e8b1c11b..51415954d 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -3,6 +3,7 @@ part of '../health.dart'; /// List of all available health data types. enum HealthDataType { ACTIVE_ENERGY_BURNED, + ATRIAL_FIBRILLATION_BURDEN, AUDIOGRAM, BASAL_ENERGY_BURNED, BLOOD_GLUCOSE, @@ -10,6 +11,7 @@ enum HealthDataType { BLOOD_PRESSURE_DIASTOLIC, BLOOD_PRESSURE_SYSTOLIC, BODY_FAT_PERCENTAGE, + LEAN_BODY_MASS, BODY_MASS_INDEX, BODY_TEMPERATURE, BODY_WATER_MASS, @@ -18,10 +20,45 @@ enum HealthDataType { DIETARY_ENERGY_CONSUMED, DIETARY_FATS_CONSUMED, DIETARY_PROTEIN_CONSUMED, + DIETARY_FIBER, + DIETARY_SUGAR, + DIETARY_FAT_MONOUNSATURATED, + DIETARY_FAT_POLYUNSATURATED, + DIETARY_FAT_SATURATED, + DIETARY_CHOLESTEROL, + DIETARY_VITAMIN_A, + DIETARY_THIAMIN, + DIETARY_RIBOFLAVIN, + DIETARY_NIACIN, + DIETARY_PANTOTHENIC_ACID, + DIETARY_VITAMIN_B6, + DIETARY_BIOTIN, + DIETARY_VITAMIN_B12, + DIETARY_VITAMIN_C, + DIETARY_VITAMIN_D, + DIETARY_VITAMIN_E, + DIETARY_VITAMIN_K, + DIETARY_FOLATE, + DIETARY_CALCIUM, + DIETARY_CHLORIDE, + DIETARY_IRON, + DIETARY_MAGNESIUM, + DIETARY_PHOSPHORUS, + DIETARY_POTASSIUM, + DIETARY_SODIUM, + DIETARY_ZINC, + DIETARY_CHROMIUM, + DIETARY_COPPER, + DIETARY_IODINE, + DIETARY_MANGANESE, + DIETARY_MOLYBDENUM, + DIETARY_SELENIUM, FORCED_EXPIRATORY_VOLUME, HEART_RATE, HEART_RATE_VARIABILITY_SDNN, + HEART_RATE_VARIABILITY_RMSSD, HEIGHT, + INSULIN_DELIVERY, RESTING_HEART_RATE, RESPIRATORY_RATE, PERIPHERAL_PERFUSION_INDEX, @@ -33,21 +70,19 @@ enum HealthDataType { DISTANCE_SWIMMING, DISTANCE_CYCLING, FLIGHTS_CLIMBED, - MOVE_MINUTES, DISTANCE_DELTA, MINDFULNESS, WATER, - SLEEP_IN_BED, SLEEP_ASLEEP, - SLEEP_ASLEEP_CORE, - SLEEP_ASLEEP_DEEP, - SLEEP_ASLEEP_REM, + SLEEP_AWAKE_IN_BED, SLEEP_AWAKE, - SLEEP_LIGHT, SLEEP_DEEP, - SLEEP_REM, + SLEEP_IN_BED, + SLEEP_LIGHT, SLEEP_OUT_OF_BED, + SLEEP_REM, SLEEP_SESSION, + SLEEP_UNKNOWN, EXERCISE_TIME, WORKOUT, HEADACHE_NOT_PRESENT, @@ -56,6 +91,13 @@ enum HealthDataType { HEADACHE_SEVERE, HEADACHE_UNSPECIFIED, NUTRITION, + // HealthKit Characteristics + GENDER, + BIRTH_DATE, + BLOOD_TYPE, + MENSTRUATION_FLOW, + WATER_TEMPERATURE, + UNDERWATER_DEPTH, // Heart Rate events (specific to Apple Watch) HIGH_HEART_RATE_EVENT, @@ -81,6 +123,7 @@ enum HealthDataAccess { /// List of data types available on iOS. const List dataTypeKeysIOS = [ HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.ATRIAL_FIBRILLATION_BURDEN, HealthDataType.AUDIOGRAM, HealthDataType.BASAL_ENERGY_BURNED, HealthDataType.BLOOD_GLUCOSE, @@ -88,6 +131,7 @@ const List dataTypeKeysIOS = [ HealthDataType.BLOOD_PRESSURE_DIASTOLIC, HealthDataType.BLOOD_PRESSURE_SYSTOLIC, HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.LEAN_BODY_MASS, HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.DIETARY_CARBS_CONSUMED, @@ -95,11 +139,45 @@ const List dataTypeKeysIOS = [ HealthDataType.DIETARY_ENERGY_CONSUMED, HealthDataType.DIETARY_FATS_CONSUMED, HealthDataType.DIETARY_PROTEIN_CONSUMED, + HealthDataType.DIETARY_FIBER, + HealthDataType.DIETARY_SUGAR, + HealthDataType.DIETARY_FAT_MONOUNSATURATED, + HealthDataType.DIETARY_FAT_POLYUNSATURATED, + HealthDataType.DIETARY_FAT_SATURATED, + HealthDataType.DIETARY_CHOLESTEROL, + HealthDataType.DIETARY_VITAMIN_A, + HealthDataType.DIETARY_THIAMIN, + HealthDataType.DIETARY_RIBOFLAVIN, + HealthDataType.DIETARY_NIACIN, + HealthDataType.DIETARY_PANTOTHENIC_ACID, + HealthDataType.DIETARY_VITAMIN_B6, + HealthDataType.DIETARY_BIOTIN, + HealthDataType.DIETARY_VITAMIN_B12, + HealthDataType.DIETARY_VITAMIN_C, + HealthDataType.DIETARY_VITAMIN_D, + HealthDataType.DIETARY_VITAMIN_E, + HealthDataType.DIETARY_VITAMIN_K, + HealthDataType.DIETARY_FOLATE, + HealthDataType.DIETARY_CALCIUM, + HealthDataType.DIETARY_CHLORIDE, + HealthDataType.DIETARY_IRON, + HealthDataType.DIETARY_MAGNESIUM, + HealthDataType.DIETARY_PHOSPHORUS, + HealthDataType.DIETARY_POTASSIUM, + HealthDataType.DIETARY_SODIUM, + HealthDataType.DIETARY_ZINC, + HealthDataType.DIETARY_CHROMIUM, + HealthDataType.DIETARY_COPPER, + HealthDataType.DIETARY_IODINE, + HealthDataType.DIETARY_MANGANESE, + HealthDataType.DIETARY_MOLYBDENUM, + HealthDataType.DIETARY_SELENIUM, HealthDataType.ELECTRODERMAL_ACTIVITY, HealthDataType.FORCED_EXPIRATORY_VOLUME, HealthDataType.HEART_RATE, HealthDataType.HEART_RATE_VARIABILITY_SDNN, HealthDataType.HEIGHT, + HealthDataType.INSULIN_DELIVERY, HealthDataType.HIGH_HEART_RATE_EVENT, HealthDataType.IRREGULAR_HEART_RATE_EVENT, HealthDataType.LOW_HEART_RATE_EVENT, @@ -115,14 +193,12 @@ const List dataTypeKeysIOS = [ HealthDataType.DISTANCE_SWIMMING, HealthDataType.DISTANCE_CYCLING, HealthDataType.MINDFULNESS, - HealthDataType.SLEEP_IN_BED, - HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_LIGHT, HealthDataType.SLEEP_REM, - HealthDataType.SLEEP_ASLEEP_CORE, - HealthDataType.SLEEP_ASLEEP_DEEP, - HealthDataType.SLEEP_ASLEEP_REM, HealthDataType.WATER, HealthDataType.EXERCISE_TIME, HealthDataType.WORKOUT, @@ -133,6 +209,12 @@ const List dataTypeKeysIOS = [ HealthDataType.HEADACHE_UNSPECIFIED, HealthDataType.ELECTROCARDIOGRAM, HealthDataType.NUTRITION, + HealthDataType.GENDER, + HealthDataType.BIRTH_DATE, + HealthDataType.BLOOD_TYPE, + HealthDataType.MENSTRUATION_FLOW, + HealthDataType.WATER_TEMPERATURE, + HealthDataType.UNDERWATER_DEPTH, HealthDataType.TIME_IN_DAYLIGHT ]; @@ -144,23 +226,25 @@ const List dataTypeKeysAndroid = [ HealthDataType.BLOOD_PRESSURE_DIASTOLIC, HealthDataType.BLOOD_PRESSURE_SYSTOLIC, HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.LEAN_BODY_MASS, HealthDataType.BODY_MASS_INDEX, HealthDataType.BODY_TEMPERATURE, HealthDataType.BODY_WATER_MASS, HealthDataType.HEART_RATE, + HealthDataType.HEART_RATE_VARIABILITY_RMSSD, HealthDataType.HEIGHT, HealthDataType.STEPS, HealthDataType.WEIGHT, - HealthDataType.MOVE_MINUTES, HealthDataType.DISTANCE_DELTA, - HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, - HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_AWAKE_IN_BED, + HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_DEEP, HealthDataType.SLEEP_LIGHT, - HealthDataType.SLEEP_REM, HealthDataType.SLEEP_OUT_OF_BED, + HealthDataType.SLEEP_REM, HealthDataType.SLEEP_SESSION, + HealthDataType.SLEEP_UNKNOWN, HealthDataType.WATER, HealthDataType.WORKOUT, HealthDataType.RESTING_HEART_RATE, @@ -169,11 +253,13 @@ const List dataTypeKeysAndroid = [ HealthDataType.RESPIRATORY_RATE, HealthDataType.NUTRITION, HealthDataType.TOTAL_CALORIES_BURNED, + HealthDataType.MENSTRUATION_FLOW, ]; /// Maps a [HealthDataType] to a [HealthDataUnit]. const Map dataTypeToUnit = { HealthDataType.ACTIVE_ENERGY_BURNED: HealthDataUnit.KILOCALORIE, + HealthDataType.ATRIAL_FIBRILLATION_BURDEN: HealthDataUnit.PERCENT, HealthDataType.AUDIOGRAM: HealthDataUnit.DECIBEL_HEARING_LEVEL, HealthDataType.BASAL_ENERGY_BURNED: HealthDataUnit.KILOCALORIE, HealthDataType.BLOOD_GLUCOSE: HealthDataUnit.MILLIGRAM_PER_DECILITER, @@ -181,6 +267,7 @@ const Map dataTypeToUnit = { HealthDataType.BLOOD_PRESSURE_DIASTOLIC: HealthDataUnit.MILLIMETER_OF_MERCURY, HealthDataType.BLOOD_PRESSURE_SYSTOLIC: HealthDataUnit.MILLIMETER_OF_MERCURY, HealthDataType.BODY_FAT_PERCENTAGE: HealthDataUnit.PERCENT, + HealthDataType.LEAN_BODY_MASS: HealthDataUnit.KILOGRAM, HealthDataType.BODY_MASS_INDEX: HealthDataUnit.NO_UNIT, HealthDataType.BODY_TEMPERATURE: HealthDataUnit.DEGREE_CELSIUS, HealthDataType.BODY_WATER_MASS: HealthDataUnit.KILOGRAM, @@ -189,12 +276,47 @@ const Map dataTypeToUnit = { HealthDataType.DIETARY_ENERGY_CONSUMED: HealthDataUnit.KILOCALORIE, HealthDataType.DIETARY_FATS_CONSUMED: HealthDataUnit.GRAM, HealthDataType.DIETARY_PROTEIN_CONSUMED: HealthDataUnit.GRAM, + HealthDataType.DIETARY_FIBER: HealthDataUnit.GRAM, + HealthDataType.DIETARY_SUGAR: HealthDataUnit.GRAM, + HealthDataType.DIETARY_FAT_MONOUNSATURATED: HealthDataUnit.GRAM, + HealthDataType.DIETARY_FAT_POLYUNSATURATED: HealthDataUnit.GRAM, + HealthDataType.DIETARY_FAT_SATURATED: HealthDataUnit.GRAM, + HealthDataType.DIETARY_CHOLESTEROL: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_A: HealthDataUnit.GRAM, + HealthDataType.DIETARY_THIAMIN: HealthDataUnit.GRAM, + HealthDataType.DIETARY_RIBOFLAVIN: HealthDataUnit.GRAM, + HealthDataType.DIETARY_NIACIN: HealthDataUnit.GRAM, + HealthDataType.DIETARY_PANTOTHENIC_ACID: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_B6: HealthDataUnit.GRAM, + HealthDataType.DIETARY_BIOTIN: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_B12: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_C: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_D: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_E: HealthDataUnit.GRAM, + HealthDataType.DIETARY_VITAMIN_K: HealthDataUnit.GRAM, + HealthDataType.DIETARY_FOLATE: HealthDataUnit.GRAM, + HealthDataType.DIETARY_CALCIUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_CHLORIDE: HealthDataUnit.GRAM, + HealthDataType.DIETARY_IRON: HealthDataUnit.GRAM, + HealthDataType.DIETARY_MAGNESIUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_PHOSPHORUS: HealthDataUnit.GRAM, + HealthDataType.DIETARY_POTASSIUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_SODIUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_ZINC: HealthDataUnit.GRAM, + HealthDataType.DIETARY_CHROMIUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_COPPER: HealthDataUnit.GRAM, + HealthDataType.DIETARY_IODINE: HealthDataUnit.GRAM, + HealthDataType.DIETARY_MANGANESE: HealthDataUnit.GRAM, + HealthDataType.DIETARY_MOLYBDENUM: HealthDataUnit.GRAM, + HealthDataType.DIETARY_SELENIUM: HealthDataUnit.GRAM, + HealthDataType.ELECTRODERMAL_ACTIVITY: HealthDataUnit.SIEMEN, HealthDataType.FORCED_EXPIRATORY_VOLUME: HealthDataUnit.LITER, HealthDataType.HEART_RATE: HealthDataUnit.BEATS_PER_MINUTE, HealthDataType.RESPIRATORY_RATE: HealthDataUnit.RESPIRATIONS_PER_MINUTE, HealthDataType.PERIPHERAL_PERFUSION_INDEX: HealthDataUnit.PERCENT, HealthDataType.HEIGHT: HealthDataUnit.METER, + HealthDataType.INSULIN_DELIVERY: HealthDataUnit.INTERNATIONAL_UNIT, HealthDataType.RESTING_HEART_RATE: HealthDataUnit.BEATS_PER_MINUTE, HealthDataType.STEPS: HealthDataUnit.COUNT, HealthDataType.WAIST_CIRCUMFERENCE: HealthDataUnit.METER, @@ -204,21 +326,19 @@ const Map dataTypeToUnit = { HealthDataType.DISTANCE_SWIMMING: HealthDataUnit.METER, HealthDataType.DISTANCE_CYCLING: HealthDataUnit.METER, HealthDataType.FLIGHTS_CLIMBED: HealthDataUnit.COUNT, - HealthDataType.MOVE_MINUTES: HealthDataUnit.MINUTE, HealthDataType.DISTANCE_DELTA: HealthDataUnit.METER, HealthDataType.WATER: HealthDataUnit.LITER, - HealthDataType.SLEEP_IN_BED: HealthDataUnit.MINUTE, HealthDataType.SLEEP_ASLEEP: HealthDataUnit.MINUTE, - HealthDataType.SLEEP_ASLEEP_CORE: HealthDataUnit.MINUTE, - HealthDataType.SLEEP_ASLEEP_DEEP: HealthDataUnit.MINUTE, - HealthDataType.SLEEP_ASLEEP_REM: HealthDataUnit.MINUTE, HealthDataType.SLEEP_AWAKE: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_AWAKE_IN_BED: HealthDataUnit.MINUTE, HealthDataType.SLEEP_DEEP: HealthDataUnit.MINUTE, - HealthDataType.SLEEP_REM: HealthDataUnit.MINUTE, - HealthDataType.SLEEP_OUT_OF_BED: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_IN_BED: HealthDataUnit.MINUTE, HealthDataType.SLEEP_LIGHT: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_OUT_OF_BED: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_REM: HealthDataUnit.MINUTE, HealthDataType.SLEEP_SESSION: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_UNKNOWN: HealthDataUnit.MINUTE, HealthDataType.MINDFULNESS: HealthDataUnit.MINUTE, HealthDataType.EXERCISE_TIME: HealthDataUnit.MINUTE, @@ -230,14 +350,22 @@ const Map dataTypeToUnit = { HealthDataType.HEADACHE_SEVERE: HealthDataUnit.MINUTE, HealthDataType.HEADACHE_UNSPECIFIED: HealthDataUnit.MINUTE, + HealthDataType.GENDER: HealthDataUnit.NO_UNIT, + HealthDataType.BIRTH_DATE: HealthDataUnit.NO_UNIT, + HealthDataType.BLOOD_TYPE: HealthDataUnit.NO_UNIT, + // Heart Rate events (specific to Apple Watch) HealthDataType.HIGH_HEART_RATE_EVENT: HealthDataUnit.NO_UNIT, HealthDataType.LOW_HEART_RATE_EVENT: HealthDataUnit.NO_UNIT, HealthDataType.IRREGULAR_HEART_RATE_EVENT: HealthDataUnit.NO_UNIT, HealthDataType.HEART_RATE_VARIABILITY_SDNN: HealthDataUnit.MILLISECOND, + HealthDataType.HEART_RATE_VARIABILITY_RMSSD: HealthDataUnit.MILLISECOND, HealthDataType.ELECTROCARDIOGRAM: HealthDataUnit.VOLT, HealthDataType.NUTRITION: HealthDataUnit.NO_UNIT, + HealthDataType.MENSTRUATION_FLOW: HealthDataUnit.NO_UNIT, + HealthDataType.WATER_TEMPERATURE: HealthDataUnit.DEGREE_CELSIUS, + HealthDataType.UNDERWATER_DEPTH: HealthDataUnit.METER, // Health Connect HealthDataType.TOTAL_CALORIES_BURNED: HealthDataUnit.KILOCALORIE, @@ -337,26 +465,26 @@ enum HealthWorkoutActivityType { // Commented for which platform the type are supported // Both + AMERICAN_FOOTBALL, ARCHERY, + AUSTRALIAN_FOOTBALL, BADMINTON, BASEBALL, BASKETBALL, BIKING, // This also entails the iOS version where it is called CYCLING BOXING, CRICKET, + CROSS_COUNTRY_SKIING, CURLING, + DOWNHILL_SKIING, ELLIPTICAL, FENCING, - AMERICAN_FOOTBALL, - AUSTRALIAN_FOOTBALL, - SOCCER, GOLF, GYMNASTICS, HANDBALL, HIGH_INTENSITY_INTERVAL_TRAINING, HIKING, HOCKEY, - SKATING, JUMP_ROPE, KICKBOXING, MARTIAL_ARTS, @@ -366,9 +494,9 @@ enum HealthWorkoutActivityType { RUGBY, RUNNING, SAILING, - CROSS_COUNTRY_SKIING, - DOWNHILL_SKIING, + SKATING, SNOWBOARDING, + SOCCER, SOFTBALL, SQUASH, STAIR_CLIMBING, @@ -381,112 +509,64 @@ enum HealthWorkoutActivityType { YOGA, // iOS only + BARRE, BOWLING, + CARDIO_DANCE, + CLIMBING, + COOLDOWN, + CORE_TRAINING, CROSS_TRAINING, - TRACK_AND_FIELD, DISC_SPORTS, - LACROSSE, - PREPARATION_AND_RECOVERY, + EQUESTRIAN_SPORTS, + FISHING, + FITNESS_GAMING, FLEXIBILITY, - COOLDOWN, - WHEELCHAIR_WALK_PACE, - WHEELCHAIR_RUN_PACE, - HAND_CYCLING, - CORE_TRAINING, FUNCTIONAL_STRENGTH_TRAINING, - TRADITIONAL_STRENGTH_TRAINING, - MIXED_CARDIO, - STAIRS, - STEP_TRAINING, - FITNESS_GAMING, - BARRE, - CARDIO_DANCE, - SOCIAL_DANCE, + HAND_CYCLING, + HUNTING, + LACROSSE, MIND_AND_BODY, + MIXED_CARDIO, + PADDLE_SPORTS, PICKLEBALL, - CLIMBING, - EQUESTRIAN_SPORTS, - FISHING, - HUNTING, PLAY, + PREPARATION_AND_RECOVERY, SNOW_SPORTS, - PADDLE_SPORTS, - SURFING_SPORTS, + SOCIAL_DANCE, + STAIRS, + STEP_TRAINING, + SURFING, + TAI_CHI, + TRACK_AND_FIELD, + TRADITIONAL_STRENGTH_TRAINING, WATER_FITNESS, WATER_SPORTS, - TAI_CHI, + WHEELCHAIR_RUN_PACE, + WHEELCHAIR_WALK_PACE, WRESTLING, + UNDERWATER_DIVING, // Android only - AEROBICS, - BIATHLON, - BIKING_HAND, - BIKING_MOUNTAIN, - BIKING_ROAD, - BIKING_SPINNING, BIKING_STATIONARY, - BIKING_UTILITY, CALISTHENICS, - CIRCUIT_TRAINING, - CROSS_FIT, DANCING, - DIVING, - ELEVATOR, - ERGOMETER, - ESCALATOR, FRISBEE_DISC, - GARDENING, GUIDED_BREATHING, - HORSEBACK_RIDING, - HOUSEWORK, - INTERVAL_TRAINING, - IN_VEHICLE, ICE_SKATING, - KAYAKING, - KETTLEBELL_TRAINING, - KICK_SCOOTER, - KITE_SURFING, - MEDITATION, - MIXED_MARTIAL_ARTS, - P90X, PARAGLIDING, - POLO, ROCK_CLIMBING, // on iOS this is the same as CLIMBING ROWING_MACHINE, - RUNNING_JOGGING, // on iOS this is the same as RUNNING - RUNNING_SAND, // on iOS this is the same as RUNNING RUNNING_TREADMILL, // on iOS this is the same as RUNNING SCUBA_DIVING, - SKATING_CROSS, // on iOS this is the same as SKATING - SKATING_INDOOR, // on iOS this is the same as SKATING - SKATING_INLINE, // on iOS this is the same as SKATING SKIING, - SKIING_BACK_COUNTRY, - SKIING_KITE, - SKIING_ROLLER, - SLEDDING, - SNOWMOBILE, SNOWSHOEING, STAIR_CLIMBING_MACHINE, - STANDUP_PADDLEBOARDING, - STILL, STRENGTH_TRAINING, - SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - TEAM_SPORTS, - TILTING, - VOLLEYBALL_BEACH, - VOLLEYBALL_INDOOR, - WAKEBOARDING, - WALKING_FITNESS, - WALKING_NORDIC, - WALKING_STROLLER, WALKING_TREADMILL, WEIGHTLIFTING, WHEELCHAIR, - WINDSURFING, - ZUMBA, // OTHER, @@ -512,6 +592,13 @@ enum ElectrocardiogramClassification { UNRECOGNIZED, } +/// Types of insulin delivery reason +enum InsulinDeliveryReason { + NOT_SET, + BASAL, + BOLUS, +} + /// Extension to assign numbers to [ElectrocardiogramClassification]s extension ElectrocardiogramClassificationValue on ElectrocardiogramClassification { diff --git a/packages/health/lib/src/workout_summary.dart b/packages/health/lib/src/workout_summary.dart index c36613802..14682a402 100644 --- a/packages/health/lib/src/workout_summary.dart +++ b/packages/health/lib/src/workout_summary.dart @@ -6,7 +6,7 @@ part of '../health.dart'; /// * [totalDistance] - The total distance that was traveled during a workout. /// * [totalEnergyBurned] - The amount of energy that was burned during a workout. /// * [totalSteps] - The number of steps during a workout. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class WorkoutSummary { /// Workout type. String workoutType; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index cc118c503..ed206e548 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,6 +1,6 @@ name: health -description: Wrapper for HealthKit on iOS and Google Fit and Health Connect on Android. -version: 10.2.0 +description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. +version: 12.0.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: @@ -10,10 +10,10 @@ environment: dependencies: flutter: sdk: flutter - intl: '>=0.18.0 <0.20.0' - device_info_plus: '>=9.0.0 <11.0.0' + intl: '>=0.18.0 <0.21.0' + device_info_plus: '>=9.0.0 <12.0.0' json_annotation: ^4.8.0 - carp_serializable: ^1.1.0 # polymorphic json serialization + carp_serializable: ^2.0.0 # polymorphic json serialization dev_dependencies: flutter_test: @@ -26,6 +26,7 @@ dev_dependencies: # dart run build_runner build --delete-conflicting-outputs build_runner: any json_serializable: any + mocktail: ^1.0.4 flutter: plugin: diff --git a/packages/health/test/health_test.dart b/packages/health/test/health_test.dart index 8b1378917..dc7c88cbc 100644 --- a/packages/health/test/health_test.dart +++ b/packages/health/test/health_test.dart @@ -1 +1,337 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:health/health.dart'; +import 'package:carp_serializable/carp_serializable.dart'; +import 'mocks/device_info_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('HealthDataPoint fromJson Tests', () { + + //Instantiate Health class with the Mock + final health = Health(deviceInfo: MockDeviceInfoPlugin()); + setUpAll(() async { + await health.configure(); + }); + test('Test WorkoutHealthValue', () async { + var entry = { + "uuid": "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0", + "value": { + "__type": "WorkoutHealthValue", + "workoutActivityType": "AMERICAN_FOOTBALL", + "totalEnergyBurned": 100, + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": 2000, + "totalDistanceUnit": "METER" + }, + "type": "WORKOUT", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "756B1A7A-C972-4BDB-9748-0D4749CF299C", + "sourceId": "com.apple.Health", + "sourceName": "Salud", + "recordingMethod": "manual", + "workoutSummary": { + "workoutType": "AMERICAN_FOOTBALL", + "totalDistance": 2000, + "totalEnergyBurned": 100, + "totalSteps": 0 + } + }; + + var hdp = HealthDataPoint.fromJson(entry); + + expect(hdp.uuid, "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0"); + expect(hdp.type, HealthDataType.WORKOUT); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "756B1A7A-C972-4BDB-9748-0D4749CF299C"); + expect(hdp.sourceId, "com.apple.Health"); + expect(hdp.sourceName, "Salud"); + expect(hdp.recordingMethod, RecordingMethod.manual); + + expect(hdp.value, isA()); + expect((hdp.value as WorkoutHealthValue).workoutActivityType, + HealthWorkoutActivityType.AMERICAN_FOOTBALL); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurned, 100); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurnedUnit, + HealthDataUnit.KILOCALORIE); + expect((hdp.value as WorkoutHealthValue).totalDistance, 2000); + expect((hdp.value as WorkoutHealthValue).totalDistanceUnit, + HealthDataUnit.METER); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + + + }); + test('Test NumericHealthValue', () { + final json = { + "uuid": "some-uuid-1", + "value": {"__type": "NumericHealthValue", "numericValue": 123.45}, + "type": "HEART_RATE", + "unit": "COUNT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-1"); + expect(hdp.type, HealthDataType.HEART_RATE); + expect(hdp.unit, HealthDataUnit.COUNT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.automatic); + + expect(hdp.value, isA()); + expect((hdp.value as NumericHealthValue).numericValue, 123.45); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test AudiogramHealthValue', () { + final json = { + "uuid": "some-uuid-2", + "value": { + "__type": "AudiogramHealthValue", + "frequencies": [1000.0, 2000.0, 3000.0], + "leftEarSensitivities": [20.0, 25.0, 30.0], + "rightEarSensitivities": [15.0, 20.0, 25.0] + }, + "type": "AUDIOGRAM", + "unit": "DECIBEL_HEARING_LEVEL", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-2"); + expect(hdp.type, HealthDataType.AUDIOGRAM); + expect(hdp.unit, HealthDataUnit.DECIBEL_HEARING_LEVEL); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final audiogramValue = hdp.value as AudiogramHealthValue; + expect(audiogramValue.frequencies, [1000.0, 2000.0, 3000.0]); + expect(audiogramValue.leftEarSensitivities, [20.0, 25.0, 30.0]); + expect(audiogramValue.rightEarSensitivities, [15.0, 20.0, 25.0]); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test ElectrocardiogramHealthValue', () { + final json = { + "uuid": "some-uuid-3", + "value": { + "__type": "ElectrocardiogramHealthValue", + "voltageValues": [ + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.1, + "timeSinceSampleStart": 0.01 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.2, + "timeSinceSampleStart": 0.02 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.3, + "timeSinceSampleStart": 0.03 + } + ], + }, + "type": "ELECTROCARDIOGRAM", + "unit": "VOLT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "active" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-3"); + expect(hdp.type, HealthDataType.ELECTROCARDIOGRAM); + expect(hdp.unit, HealthDataUnit.VOLT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.active); + expect(hdp.value, isA()); + + final ecgValue = hdp.value as ElectrocardiogramHealthValue; + expect(ecgValue.voltageValues.length, 3); + expect(ecgValue.voltageValues[0], isA()); + expect(ecgValue.voltageValues[0].voltage, 0.1); + expect(ecgValue.voltageValues[0].timeSinceSampleStart, 0.01); + expect(ecgValue.voltageValues[1].voltage, 0.2); + expect(ecgValue.voltageValues[1].timeSinceSampleStart, 0.02); + expect(ecgValue.voltageValues[2].voltage, 0.3); + expect(ecgValue.voltageValues[2].timeSinceSampleStart, 0.03); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test NutritionHealthValue', () { + final json = { + "uuid": "some-uuid-4", + "value": { + "__type": "NutritionHealthValue", + "calories": 500.0, + "carbs": 60.0, + "protein": 20.0, + "fat": 30.0, + "caffeine": 100.0, + "vitaminA": 20.0, + "b1Thiamine": 20.0, + "b2Riboflavin": 20.0, + "b3Niacin": 20.0, + "b5PantothenicAcid": 20.0, + "b6Pyridoxine": 20.0, + "b7Biotin": 20.0, + "b9Folate": 20.0, + "b12Cobalamin": 20.0, + "vitaminC": 20.0, + "vitaminD": 20.0, + "vitaminE": 20.0, + "vitaminK": 20.0, + "calcium": 20.0, + "cholesterol": 20.0, + "chloride": 20.0, + "chromium": 20.0, + "copper": 20.0, + "fatUnsaturated": 20.0, + "fatMonounsaturated": 20.0, + "fatPolyunsaturated": 20.0, + "fatSaturated": 20.0, + "fatTransMonoenoic": 20.0, + "fiber": 20.0, + "iodine": 20.0, + "iron": 20.0, + "magnesium": 20.0, + "manganese": 20.0, + "molybdenum": 20.0, + "phosphorus": 20.0, + "potassium": 20.0, + "selenium": 20.0, + "sodium": 20.0, + "sugar": 20.0, + "water": 20.0, + "zinc": 20.0 + }, + "type": "NUTRITION", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + + final hdp = HealthDataPoint.fromJson(json); + expect(hdp.uuid, "some-uuid-4"); + expect(hdp.type, HealthDataType.NUTRITION); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final nutritionValue = hdp.value as NutritionHealthValue; + expect(nutritionValue.calories, 500.0); + expect(nutritionValue.carbs, 60.0); + expect(nutritionValue.protein, 20.0); + expect(nutritionValue.fat, 30.0); + expect(nutritionValue.caffeine, 100.0); + expect(nutritionValue.vitaminA, 20.0); + expect(nutritionValue.b1Thiamine, 20.0); + expect(nutritionValue.b2Riboflavin, 20.0); + expect(nutritionValue.b3Niacin, 20.0); + expect(nutritionValue.b5PantothenicAcid, 20.0); + expect(nutritionValue.b6Pyridoxine, 20.0); + expect(nutritionValue.b7Biotin, 20.0); + expect(nutritionValue.b9Folate, 20.0); + expect(nutritionValue.b12Cobalamin, 20.0); + expect(nutritionValue.vitaminC, 20.0); + expect(nutritionValue.vitaminD, 20.0); + expect(nutritionValue.vitaminE, 20.0); + expect(nutritionValue.vitaminK, 20.0); + expect(nutritionValue.calcium, 20.0); + expect(nutritionValue.cholesterol, 20.0); + expect(nutritionValue.chloride, 20.0); + expect(nutritionValue.chromium, 20.0); + expect(nutritionValue.copper, 20.0); + expect(nutritionValue.fatUnsaturated, 20.0); + expect(nutritionValue.fatMonounsaturated, 20.0); + expect(nutritionValue.fatPolyunsaturated, 20.0); + expect(nutritionValue.fatSaturated, 20.0); + expect(nutritionValue.fatTransMonoenoic, 20.0); + expect(nutritionValue.fiber, 20.0); + expect(nutritionValue.iodine, 20.0); + expect(nutritionValue.iron, 20.0); + expect(nutritionValue.magnesium, 20.0); + expect(nutritionValue.manganese, 20.0); + expect(nutritionValue.molybdenum, 20.0); + expect(nutritionValue.phosphorus, 20.0); + expect(nutritionValue.potassium, 20.0); + expect(nutritionValue.selenium, 20.0); + expect(nutritionValue.sodium, 20.0); + expect(nutritionValue.sugar, 20.0); + expect(nutritionValue.water, 20.0); + expect(nutritionValue.zinc, 20.0); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test HealthValue error handling', () { + final json = { + "uuid": "some-uuid-error", + "value": { + "__type": "UnknownHealthValue", // This should throw an error + "numericValue": 123.45 + }, + "type": "HEART_RATE", + "unit": "COUNT_PER_MINUTE", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + expect( + () => HealthDataPoint.fromJson(json), + throwsA( + isA())); //Expect SerializationException + }); + }); +} diff --git a/packages/health/test/mocks/device_info_mock.dart b/packages/health/test/mocks/device_info_mock.dart new file mode 100644 index 000000000..019a36012 --- /dev/null +++ b/packages/health/test/mocks/device_info_mock.dart @@ -0,0 +1,60 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +class MockDeviceInfoPlugin extends Mock implements DeviceInfoPlugin { + @override + Future get androidInfo => + Future.value(AndroidDeviceInfo.fromMap({ + 'id': 'mock-android-id', + 'version': { + 'baseOS': 'mock-baseOS', + 'codename': 'mock-codename', + 'incremental': 'mock-incremental', + 'previewSdkInt': 23, + 'release': 'mock-release', + 'sdkInt': 30, + 'securityPatch': 'mock-securityPatch', + }, + 'board': 'mock-board', + 'bootloader': 'mock-bootloader', + 'brand': 'mock-brand', + 'device': 'mock-device', + 'display': 'mock-display', + 'fingerprint': 'mock-fingerprint', + 'hardware': 'mock-hardware', + 'host': 'mock-host', + 'manufacturer': 'mock-manufacturer', + 'model': 'mock-model', + 'product': 'mock-product', + 'supported32BitAbis': [], + 'supported64BitAbis': [], + 'supportedAbis': [], + 'tags': 'mock-tags', + 'type': 'mock-type', + 'isPhysicalDevice': true, + 'systemFeatures': [], + 'serialNumber': 'mock-serial', + 'isLowRamDevice': false, + })); + + + @override + Future get iosInfo => Future.value(IosDeviceInfo.fromMap({ + 'name': 'mock-ios-name', + 'systemName': 'mock-ios-systemName', + 'systemVersion': '16.0', + 'model': 'mock-ios-model', + 'modelName': 'mock-ios-modelName', + 'localizedModel': 'mock-ios-localizedModel', + 'identifierForVendor': 'mock-ios-id', + 'isPhysicalDevice': true, + 'isiOSAppOnMac': false, + 'utsname': { + 'sysname': 'mock-ios-sysname', + 'nodename': 'mock-ios-nodename', + 'release': 'mock-ios-release', + 'version': 'mock-ios-version', + 'machine': 'mock-ios-machine', + }, + })); +} \ No newline at end of file diff --git a/packages/light/android/build.gradle b/packages/light/android/build.gradle index 0d2c15b97..d256b7247 100644 --- a/packages/light/android/build.gradle +++ b/packages/light/android/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' android { compileSdkVersion 31 + namespace 'dk.cachet.light' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/mobility_features/CHANGELOG.md b/packages/mobility_features/CHANGELOG.md index 7555c34ad..aa09baeed 100644 --- a/packages/mobility_features/CHANGELOG.md +++ b/packages/mobility_features/CHANGELOG.md @@ -1,114 +1,126 @@ +## 6.0.0 + +* using carp_serialization for JSON serialization resulting in a new JSON schema for serialization +* upgrade of example app to use new Flutter Android Gradle build setup +* improvements of code structure and documentation + +## 5.0.0 + +* upgrade to Dart 3.2 and Flutter 3.16 +* improvements to API docs +* improvements to example apps (permissions - PR [#971](https://github.com/cph-cachet/flutter-plugins/pull/971)). + ## 4.0.1 -- Fixed formatting -- Lowered minSdk +* Fixed formatting +* Lowered minSdk ## 4.0.0 -- Updated kotlin and AGP -- Upgraded example app to `carp_background_location` 4.0.0 -- Implemented minor fixes +* Updated kotlin and AGP +* Upgraded example app to `carp_background_location` 4.0.0 +* Implemented minor fixes ## 3.1.0 -- improvement to `MobilityContext` API. -- misc updates to documentation. +* improvement to `MobilityContext` API. +* misc updates to documentation. ## 3.0.0+2 -- update to null-safety -- rename of `MobilityFactory` to `MobilityFeatures`and using the standard Dart singleton syntax for `MobilityFeatures()`. -- misc updates to documentation +* update to null-safety +* rename of `MobilityFactory` to `MobilityFeatures`and using the standard Dart singleton syntax for `MobilityFeatures()`. +* misc updates to documentation ## 2.0.6+1 -- removal of exception -- update to use `carp_background_location` +* removal of exception +* update to use `carp_background_location` ## 2.0.5 -- Documentation -- Added images and changed documentation somewhat +* Documentation +* Added images and changed documentation somewhat ## 2.0.4 -- Move Consolidation -- Moves are now recomputed each time a stop is computed -- This means that the number of moves is always one less that the number of stops +* Move Consolidation +* Moves are now recomputed each time a stop is computed +* This means that the number of moves is always one less that the number of stops ## 2.0.3 -- Move Calculation -- Fixed a bug when creating moves between two stops belonging to the same place -- To avoid inaccuracy distance as a resulting of noisy readings when inside buildings, this path should be computed as a straight line, rather than from a list of points +* Move Calculation +* Fixed a bug when creating moves between two stops belonging to the same place +* To avoid inaccuracy distance as a resulting of noisy readings when inside buildings, this path should be computed as a straight line, rather than from a list of points ## 2.0.2 -- Stop merging -- Implemented stop merging to prevent gaps in the data -- This was especially a problem on iOS devices during the night, where location sampling is automatically limited by the OS -- Gaps in the data during the night would cause the home stay feature to be very unreliable +* Stop merging +* Implemented stop merging to prevent gaps in the data +* This was especially a problem on iOS devices during the night, where location sampling is automatically limited by the OS +* Gaps in the data during the night would cause the home stay feature to be very unreliable ## 2.0.1 -- Stream-based API -- Removed Routine Index temporarily +* Stream-based API +* Removed Routine Index temporarily ## 2.0.0 -- Stream-based API -- The API is now fully streaming-based. +* Stream-based API +* The API is now fully streaming-based. ## 1.3.4 -- Flushing data -- Fixed an error where location samples were being flushed when they shouldn't +* Flushing data +* Fixed an error where location samples were being flushed when they shouldn't ## 1.3.3 -- Dependencies -- Updated dependencies +* Dependencies +* Updated dependencies ## 1.3.2 -- Streaming based API -- Renamed GeoPosition to GeoLocation due to naming conflicts with another package. +* Streaming based API +* Renamed GeoPosition to GeoLocation due to naming conflicts with another package. ## 1.3.0 -- Streaming based API -- Refactored API to support streaming -- An example app is now included +* Streaming based API +* Refactored API to support streaming +* An example app is now included ## 1.2.0 -- Restructuring -- MobilitySerializer is now private. +* Restructuring +* MobilitySerializer is now private. ## 1.1.5 -- Major refactoring -- Renamed and refactored classes such as Location and SingleLocationPoint to GeoPosition and LocationSample respectively. +* Major refactoring +* Renamed and refactored classes such as Location and SingleLocationPoint to GeoPosition and LocationSample respectively. ## 1.1.0 -- Private classes -- Made a series of classes private such that they cannot be instantiated from outside the package +* Private classes +* Made a series of classes private such that they cannot be instantiated from outside the package ## 1.0.0 -- Formatting -- Fixed a series of formatting issues which caused the package to score lower on pub.dev -- Upgraded the release number to 1.x.x to increase the package score on pub.dev +* Formatting +* Fixed a series of formatting issues which caused the package to score lower on pub.dev +* Upgraded the release number to 1.x.x to increase the package score on pub.dev ## 0.1.5 -- Private constructor. -- The Mobility Context constructor is now private -- A Mobility Context should always be instantiated via the ContextGenerator class. +* Private constructor. +* The Mobility Context constructor is now private +* A Mobility Context should always be instantiated via the ContextGenerator class. ## 0.1.0 -- First release. -- The first official release with working unit tests -- Includes a minimalistic API which allows the application programmer to generate features with very few lines of code. +* First release. +* The first official release with working unit tests +* Includes a minimalistic API which allows the application programmer to generate features with very few lines of code. diff --git a/packages/mobility_features/LICENSE b/packages/mobility_features/LICENSE index c0eab2edb..e584a9db2 100644 --- a/packages/mobility_features/LICENSE +++ b/packages/mobility_features/LICENSE @@ -1,6 +1,6 @@ MIT License. -Copyright 2018-2021 Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). +Copyright 2018-2024 the Technical University of Denmark (DTU). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ”Software”), to deal in the Software without restriction, including without limitation diff --git a/packages/mobility_features/README.md b/packages/mobility_features/README.md index e7cfeae9f..c3196acb6 100644 --- a/packages/mobility_features/README.md +++ b/packages/mobility_features/README.md @@ -1,11 +1,27 @@ # Mobility Features +This plugin supports the realtime calculation of mobility features based on location tracking of a phone. +The following location features are collected: + +* places +* stops +* moves + +From this, a set of derived features are calculated: + +* number of significant places +* home sStay +* location entropy +* normalized location entropy +* distance traveled + +Read more on the [theoretical background](#theoretical-background) on these mobility features below. + ## Setup -The Mobility Features package is designed to work independent of the location plugin. You may choose you own location plugin, since you may already use this in your app. +The Mobility Features package is designed to work independent of the location plugin. You may choose you own location plugin, since you may already use this in your app. -In the example app we use our own plugin [`carp_background_location`](https://pub.dev/packages/carp_background_location) which works on both Android and iOS as of August 2020. However, the -[location](https://pub.dev/packages/location) plugin will also work. The important thing, however, is to make sure that the app runs in the backgound. On Android this is tied to running the app as a foregound service. +In the example app we use our own plugin [carp_background_location](https://pub.dev/packages/carp_background_location) which works on both Android and iOS as of August 2020. However, the [location](https://pub.dev/packages/location) plugin will also work. The important thing, however, is to make sure that the app runs in the background. On Android this is tied to running the app as a foreground service. Add the package to your `pubspec.yaml` file and import the package @@ -15,7 +31,6 @@ import 'package:mobility_features/mobility_features.dart'; The plugin works as a singleton and can be accessed using `MobilityFeatures()` in the code. - ### Step 1 - Configuration of parameters The following configurations can be made, which will influence the algorithms for producing features: @@ -38,32 +53,27 @@ void initState() { } ``` -Features computation is triggered when the user moves around and change their geo-position by a certain distance (stop distance). +Features computation is triggered when the user moves around and change their geo-position by a certain distance (stop distance). If the stop was long enough (stop duration) the stop will be saved. Places are computed by grouping stops based on distance between them (place radius) -Common for these parameters is that their value depend on what you are trying to capture. +Common for these parameters is that their value depend on what you are trying to capture. Low parameter values will make the features more fine-grained but will trigger computation more often and will likely also lead to noisy features. For example, given a low stop duration, stopping for a red light in traffic will count as a stop. Such granularity will be irrelevant for many use cases, but may be useful if questions such as "Do a user take the same route to work every day?" - ### Step 2 - Set up location streaming -Collection of location data is not directly supported by this package, for this you have to use a location plugin such as [`carp_background_location`](https://pub.dev/packages/carp_background_location). You can to convert from whichever location object is used by the location plugin to a `LocationSample` object. -Next, you can start listening to location updates and subscribe to the `MobilityFeatures()`'s `contextStream` to be be notified each time a new set of features has been computed. +Collection of location data is not directly supported by this package, for this you have to use a location plugin such as [carp_background_location](https://pub.dev/packages/carp_background_location). You can to convert from whichever location object is used by the location plugin to a `LocationSample` object. +Next, you can start listening to location updates and subscribe to the `contextStream` to be be notified each time a new set of features has been computed. -Below is shown an example using the [`carp_background_location`](https://pub.dev/packages/carp_background_location) plugin, where a `LocationDto` stream is converted into a `LocationSample` stream by using a map-function. +Below is shown an example using the [carp_background_location](https://pub.dev/packages/carp_background_location) plugin, where a `LocationDto` stream is converted into a `LocationSample` stream by using a map-function. ```dart /// Set up streams: - /// * Location streaming to MobilityContext - /// * Subscribe to MobilityContext updates + /// * Location streaming to MobilityContext + /// * Subscribe to MobilityContext updates void streamInit() async { locationStream = LocationManager().locationStream; - // subscribe to location stream - in case this is needed in the app - if (locationSubscription != null) locationSubscription.cancel(); - locationSubscription = locationStream.listen(onLocationUpdate); - // start the location service (specific to carp_background_location) await LocationManager().start(); @@ -80,16 +90,13 @@ Below is shown an example using the [`carp_background_location`](https://pub.dev mobilitySubscription = MobilityFeatures().contextStream.listen(onMobilityContext); } - - /// Called whenever location changes. - void onLocationUpdate(LocationDto dto) { - print(dtoToString(dto)); - } ``` -### Step 3 - Handle mobility +> **NOTE** that access to location data needs permissions from the OS. This is **NOT** handled by the plugin but should be handled on an app-level. See the example app for this. Note also, that permissions for access location "ALWAYS" needs to be granted by the user in order to collect location information in the background. + +### Step 3 - Listen to mobility features -A call-back method is used to handle incoming MobilityContext objects: +The call-back method `onMobilityContext` is used to process the stream of `MobilityContext` objects: ```dart /// Handle incoming contexts @@ -99,62 +106,58 @@ void onMobilityContext(MobilityContext context) { } ``` -All features are implemented as getters for a `MobilityContext` object. - -```dart -/// Location features -context.places; -context.stops; -context.moves; - -/// Derived features -context.numberOfSignificantPlaces; -context.homeStay; -context.entropy; -context.normalizedEntropy; -context.distanceTravelled; +Mobility features are accessible in the `MobilityContext` object which can be serialized to JSON using the `toJson()` method: + +```json +{ + "timestamp": "2024-09-26T10:56:21.397768", + "date": "2020-01-01T00:00:00.000", + "numberOfStops": 2, + "numberOfMoves": 1, + "numberOfSignificantPlaces": 2, + "locationVariance": 0.00011097661986704458, + "entropy": 0.6365141682948128, + "normalizedEntropy": 0.9182958340544894, + "homeStay": 0.64, + "distanceTraveled": 0.0 +} ``` -## Example -The example application included in the package shows the feature values, including separate pages for stops, moves and places. - -![](https://raw.githubusercontent.com/cph-cachet/flutter-plugins/master/packages/mobility_features/images/features.jpeg) -![](https://raw.githubusercontent.com/cph-cachet/flutter-plugins/master/packages/mobility_features/images/stops.jpeg) -![](https://raw.githubusercontent.com/cph-cachet/flutter-plugins/master/packages/mobility_features/images/places.jpeg) -![](https://raw.githubusercontent.com/cph-cachet/flutter-plugins/master/packages/mobility_features/images/moves.jpeg) - - ## Feature errors -When a feature cannot be evaluated, it will result in a value of -1.0. +When a feature cannot be calculated, it will result a value of `-1.0`. -Examples: +For example: -* The Home Stay feature requires at least *some* data to be collected between 00:00 and 06:00 otherwise the feature cannot be evaluated. +* The Home Stay feature requires at least *some* data to be collected between 00:00 and 06:00 otherwise the feature cannot be evaluated. * The Entropy and Normalized Entropy features require at least 2 places to be evaluated. If only a single place was found, this will result in an Entropy of 0. -## Theorical Background +## Example -For mental health research, location data, together with a time component, -both collected from the user’s smartphone, can be reduced to certain behavioral -features pertaining to the user’s mobility. -These features can be used to diagnose patients suffering from mental disorders such as depression. +The example application included in the package shows the feature values, including separate pages for stops, moves and places. +It also illustrates how to ask the user for permissions to access location data, also when the app is in the background. + +![mobility_app_1](https://raw.githubusercontent.com/cph-cachet/flutter-plugins/master/packages/mobility_features/images/app.jpeg) + +## Theoretical Background ### Location Features -The mobility features which will be used are derived from GPS location data are: -* **Stop:** A collection of GPS points which together represent a visit at a known `Place` (see below) for an extended period of time. A `Stop` is defined by a location that represents the centroid of a collection of data points, from which a is created. In addition a `Stop` also has an `arrival` and a `departure` time-stamp, representing when the user arrived at the place and when the user left the place. From the arrival- and departure timestamps of the **Stop** the duration can be computed. +The mobility features are derived from GPS location data, like this: + +* **Stop:** A collection of GPS points which together represent a visit at a known `Place` (see below) for an extended period of time. A `Stop` is defined by a location that represents the centroid of a collection of data points, from which a is created. In addition a `Stop` also has an `arrival` and a `departure` time-stamp, representing when the user arrived at the place and when the user left the place. From the arrival- and departure timestamps of the `Stop` the duration can be computed. -* **Place:** A group of stops that were clustered by the DBSCAN algorithm. From the cluster of stops, the centroid of the stops can be found, i.e. the center location. In addition, it can be computed how long a user has visited a given place by summing over the duration of all the stops at that place. +* **Place:** A `Place` is a group of stops that were clustered by the DBSCAN algorithm. From the cluster of stops, the centroid of the stops can be found, i.e. the center location. In addition, it can be computed how long a user has visited a given place by summing over the duration of all the stops at that place. -* **Move:** The travel between two Stops, which the user will pass though a path of GPS points. The distance of a Move can be computed as the sum of using the haversine distance of this path. Given the distance travelled as well as departure and arrival timestamp from the Stops, the average speed at which the user traveled can be derived. +* **Move:** A `Move` is the travel between two stops represented as a path of GPS points. The distance of a `Move` can be computed as the sum of using the haversine distance of this path. Given the distance traveled as well as departure and arrival timestamp from the stops, the average speed at which the user traveled can be derived. ### Derived Features -* **Home Stay:** -The portion (percentage) of the total time elapsed since midnight which was spent at home. Elapsed time is calculated from the departure time of the last known stop. +A set of features can be derived from the location features: -* **Location Variance:** The statistical variance in the latitude- and longitudinal coordinates. +* **Home Stay:** The portion (percentage) of the total time elapsed since midnight which was spent at home. Elapsed time is calculated from the departure time of the last known stop. + +* **Location Variance:** The statistical variance in the latitude and longitudinal coordinates. * **Number of Places:** The number of places visited today. @@ -162,8 +165,4 @@ The portion (percentage) of the total time elapsed since midnight which was spen * **Normalized Entropy:** The normalized entropy with respect to time spent at places. -* **Distance Travelled:** The total distance travelled today (in meters), i.e. not limited to walking or running. - --------------- - -Author: Thomas Nilsson (tnni@dtu.dk) +* **Distance Traveled:** The total distance traveled today (in meters), i.e. not limited to walking or running. diff --git a/packages/mobility_features/analysis_options.yaml b/packages/mobility_features/analysis_options.yaml new file mode 100644 index 000000000..07e99cb85 --- /dev/null +++ b/packages/mobility_features/analysis_options.yaml @@ -0,0 +1,19 @@ +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: [build/**] + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + cancel_subscriptions: true + constant_identifier_names: false + depend_on_referenced_packages: true + avoid_print: false + use_string_in_part_of_directives: true diff --git a/packages/mobility_features/example/android/app/build.gradle b/packages/mobility_features/example/android/app/build.gradle index e5ef93d83..982ab50c7 100644 --- a/packages/mobility_features/example/android/app/build.gradle +++ b/packages/mobility_features/example/android/app/build.gradle @@ -1,63 +1,44 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 33 + namespace = "com.example.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } - lintOptions { - disable 'InvalidPackage' + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.example" - minSdkVersion 23 - targetSdkVersion 33 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + applicationId = "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + source = "../.." } diff --git a/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml b/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml index df58824d8..cf2dc551e 100644 --- a/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml +++ b/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,10 @@ + + + - - - + + - - - - - - + - + android:foregroundServiceType = "location"/> diff --git a/packages/mobility_features/example/android/build.gradle b/packages/mobility_features/example/android/build.gradle index f7eb7f63c..bc157bd1a 100644 --- a/packages/mobility_features/example/android/build.gradle +++ b/packages/mobility_features/example/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/packages/mobility_features/example/android/settings.gradle b/packages/mobility_features/example/android/settings.gradle index d3b6a4013..536165d35 100644 --- a/packages/mobility_features/example/android/settings.gradle +++ b/packages/mobility_features/example/android/settings.gradle @@ -1,15 +1,25 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -include ':app' + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/packages/mobility_features/example/lib/main.dart b/packages/mobility_features/example/lib/main.dart index d33addb1d..f7b42181d 100644 --- a/packages/mobility_features/example/lib/main.dart +++ b/packages/mobility_features/example/lib/main.dart @@ -2,6 +2,7 @@ library mobility_app; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:carp_background_location/carp_background_location.dart'; import 'package:mobility_features/mobility_features.dart'; @@ -9,12 +10,12 @@ part 'stops_page.dart'; part 'moves_page.dart'; part 'places_page.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); Widget entry(String key, String value, Icon icon) { return Container( padding: const EdgeInsets.all(2), - margin: EdgeInsets.all(3), + margin: const EdgeInsets.all(3), child: ListTile( leading: icon, title: Text(key), @@ -25,7 +26,7 @@ Widget entry(String key, String value, Icon icon) { String formatDate(DateTime date) => '${date.year}/${date.month}/${date.day}'; String interval(DateTime a, DateTime b) { - String pad(int x) => '${x.toString().padLeft(2, '0')}'; + String pad(int x) => x.toString().padLeft(2, '0'); return '${pad(a.hour)}:${pad(a.minute)}:${pad(a.second)} - ${pad(b.hour)}:${pad(b.minute)}:${pad(b.second)}'; } @@ -36,18 +37,20 @@ String formatDuration(Duration duration) { return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; } -final stopIcon = Icon(Icons.my_location); -final moveIcon = Icon(Icons.directions_walk); -final placeIcon = Icon(Icons.place); -final featuresIcon = Icon(Icons.assessment); -final homeStayIcon = Icon(Icons.home); -final distanceTravelledIcon = Icon(Icons.card_travel); -final entropyIcon = Icon(Icons.equalizer); -final varianceIcon = Icon(Icons.swap_calls); +const stopIcon = Icon(Icons.my_location); +const moveIcon = Icon(Icons.directions_walk); +const placeIcon = Icon(Icons.place); +const featuresIcon = Icon(Icons.assessment); +const homeStayIcon = Icon(Icons.home); +const distanceTraveledIcon = Icon(Icons.card_travel); +const entropyIcon = Icon(Icons.equalizer); +const varianceIcon = Icon(Icons.swap_calls); enum AppState { NO_FEATURES, CALCULATING_FEATURES, FEATURES_READY } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( @@ -55,7 +58,7 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Mobility Features Example'), + home: const HomePage(title: 'Mobility Features Example'), ); } } @@ -63,15 +66,15 @@ class MyApp extends StatelessWidget { String dtoToString(LocationDto dto) => '${dto.latitude}, ${dto.longitude} @ ${DateTime.fromMillisecondsSinceEpoch(dto.time ~/ 1)}'; -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); +class HomePage extends StatefulWidget { + const HomePage({super.key, required this.title}); final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + HomePageState createState() => HomePageState(); } -class _MyHomePageState extends State { +class HomePageState extends State { AppState _state = AppState.NO_FEATURES; int _currentIndex = 0; @@ -89,7 +92,7 @@ class _MyHomePageState extends State { super.initState(); // Set up Mobility Features - MobilityFeatures().stopDuration = Duration(seconds: 20); + MobilityFeatures().stopDuration = const Duration(seconds: 20); MobilityFeatures().placeRadius = 50.0; MobilityFeatures().stopRadius = 5.0; @@ -102,14 +105,18 @@ class _MyHomePageState extends State { } /// Set up streams: - /// * Location streaming to MobilityContext - /// * Subscribe to MobilityContext updates - void streamInit() async { - locationStream = LocationManager().locationStream; + /// * Location streaming to MobilityContext + /// * Subscribe to MobilityContext updates + Future streamInit() async { + await requestNotificationPermission(); + + // ask for location permissions, if not already granted + if (!await isLocationAlwaysGranted()) { + await requestLocationPermission(); + await askForLocationAlwaysPermission(); + } - // subscribe to location stream - in case this is needed in the app - //locationSubscription.cancel(); - locationSubscription = locationStream.listen(onLocationUpdate); + locationStream = LocationManager().locationStream; // start the location service (specific to carp_background_location) await LocationManager().start(); @@ -121,16 +128,59 @@ class _MyHomePageState extends State { DateTime.now())); // provide the [MobilityFeatures] instance with the LocationSample stream - MobilityFeatures().startListening(locationSampleStream); + await MobilityFeatures().startListening(locationSampleStream); // start listening to incoming MobilityContext objects mobilitySubscription = MobilityFeatures().contextStream.listen(onMobilityContext); } - /// Called whenever location changes. - void onLocationUpdate(LocationDto dto) { - print(dtoToString(dto)); + Future isLocationAlwaysGranted() async { + bool granted = false; + try { + granted = await Permission.locationAlways.isGranted; + } catch (e) { + print(e); + } + return granted; + } + + /// Tries to ask for "location always" permissions from the user. + /// Returns `true` if successful, `false` otherwise. + Future askForLocationAlwaysPermission() async { + bool granted = false; + try { + granted = await Permission.locationAlways.isGranted; + } catch (e) { + print(e); + } + + if (!granted) { + granted = + await Permission.locationAlways.request() == PermissionStatus.granted; + } + + return granted; + } + + Future requestLocationPermission() async { + final result = await Permission.location.request(); + + if (result == PermissionStatus.granted) { + print('GRANTED'); // ignore: avoid_print + } else { + print('NOT GRANTED'); // ignore: avoid_print + } + } + + Future requestNotificationPermission() async { + final result = await Permission.notification.request(); + + if (result == PermissionStatus.granted) { + print('NOTIFICATION GRANTED'); + } else { + print('NOTIFICATION NOT GRANTED'); + } } /// Called whenever mobility context changes. @@ -152,20 +202,20 @@ class _MyHomePageState extends State { Widget get featuresOverview { return ListView( children: [ - entry("Stops", "${_mobilityContext.stops.length}", stopIcon), - entry("Moves", "${_mobilityContext.moves.length}", moveIcon), + entry("Stops", "${_mobilityContext.stops?.length}", stopIcon), + entry("Moves", "${_mobilityContext.moves?.length}", moveIcon), entry("Significant Places", "${_mobilityContext.numberOfSignificantPlaces}", placeIcon), entry( "Home Stay", - _mobilityContext.homeStay! < 0 + _mobilityContext.homeStay == null || _mobilityContext.homeStay! < 0 ? "?" : "${(_mobilityContext.homeStay! * 100).toStringAsFixed(1)}%", homeStayIcon), entry( - "Distance Travelled", - "${(_mobilityContext.distanceTravelled! / 1000).toStringAsFixed(2)} km", - distanceTravelledIcon), + "Distance Traveled", + "${(_mobilityContext.distanceTraveled! / 1000).toStringAsFixed(2)} km", + distanceTraveledIcon), entry( "Normalized Entropy", "${_mobilityContext.normalizedEntropy?.toStringAsFixed(2)}", @@ -181,8 +231,8 @@ class _MyHomePageState extends State { List get contentNoFeatures { return [ Container( - margin: EdgeInsets.all(25), - child: Text( + margin: const EdgeInsets.all(25), + child: const Text( 'Move around to start generating features', style: TextStyle(fontSize: 20), )) @@ -192,15 +242,15 @@ class _MyHomePageState extends State { List get contentFeaturesReady { return [ Container( - margin: EdgeInsets.all(25), + margin: const EdgeInsets.all(25), child: Column(children: [ - Text( + const Text( 'Statistics for today,', style: TextStyle(fontSize: 20), ), Text( - '${formatDate(_mobilityContext.date)}', - style: TextStyle(fontSize: 20, color: Colors.blue), + formatDate(_mobilityContext.date!), + style: const TextStyle(fontSize: 20, color: Colors.blue), ), ])), Expanded(child: featuresOverview), @@ -209,10 +259,11 @@ class _MyHomePageState extends State { Widget get content { List children; - if (_state == AppState.FEATURES_READY) + if (_state == AppState.FEATURES_READY) { children = contentFeaturesReady; - else + } else { children = contentNoFeatures; + } return Column(children: children); } @@ -222,19 +273,17 @@ class _MyHomePageState extends State { }); } - Widget _navBar() { - return BottomNavigationBar( - onTap: onTabTapped, // new - currentIndex: _currentIndex, // this will be set when a new tab is tapped - type: BottomNavigationBarType.fixed, - items: [ - BottomNavigationBarItem(icon: featuresIcon, label: 'Features'), - BottomNavigationBarItem(icon: stopIcon, label: 'Stops'), - BottomNavigationBarItem(icon: placeIcon, label: 'Places'), - BottomNavigationBarItem(icon: moveIcon, label: 'Moves') - ], - ); - } + Widget get navBar => BottomNavigationBar( + onTap: onTabTapped, + currentIndex: _currentIndex, + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem(icon: featuresIcon, label: 'Features'), + BottomNavigationBarItem(icon: stopIcon, label: 'Stops'), + BottomNavigationBarItem(icon: placeIcon, label: 'Places'), + BottomNavigationBarItem(icon: moveIcon, label: 'Moves') + ], + ); @override Widget build(BuildContext context) { @@ -243,14 +292,16 @@ class _MyHomePageState extends State { List places = []; if (_state == AppState.FEATURES_READY) { - for (var x in _mobilityContext.stops) print(x); - for (var x in _mobilityContext.moves) { + for (var x in _mobilityContext.stops!) { + print(x); + } + for (var x in _mobilityContext.moves!) { print(x); print('${x.stopFrom} --> ${x.stopTo}'); } - stops = _mobilityContext.stops; - moves = _mobilityContext.moves; - places = _mobilityContext.places; + stops = _mobilityContext.stops ?? []; + moves = _mobilityContext.moves ?? []; + places = _mobilityContext.places ?? []; } List pages = [ @@ -263,12 +314,10 @@ class _MyHomePageState extends State { return Scaffold( appBar: AppBar( backgroundColor: Colors.teal, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: pages[_currentIndex], - bottomNavigationBar: _navBar(), + bottomNavigationBar: navBar, ); } } diff --git a/packages/mobility_features/example/lib/moves_page.dart b/packages/mobility_features/example/lib/moves_page.dart index 9819672f4..ce2cd257f 100644 --- a/packages/mobility_features/example/lib/moves_page.dart +++ b/packages/mobility_features/example/lib/moves_page.dart @@ -1,31 +1,23 @@ -part of mobility_app; +part of 'main.dart'; class MovesPage extends StatelessWidget { final List moves; - MovesPage(this.moves); + const MovesPage(this.moves, {super.key}); - Widget moveEntry(Move m) { - return Container( - padding: const EdgeInsets.all(2), - margin: EdgeInsets.all(3), - child: ListTile( - leading: Text('Place ${m.stopFrom.placeId} → ${m.stopTo.placeId}'), - title: Text('${m.distance?.toInt()} meters'), - trailing: Text('${formatDuration(m.duration)}'), - )); - } + Widget moveEntry(Move m) => Container( + padding: const EdgeInsets.all(2), + margin: const EdgeInsets.all(3), + child: ListTile( + leading: Text('Place ${m.stopFrom.placeId} → ${m.stopTo.placeId}'), + title: Text('${m.distance?.toInt()} meters'), + trailing: Text(formatDuration(m.duration)), + )); - Widget list() { - return ListView.builder( - itemCount: moves.length, - itemBuilder: (ctx, index) => moveEntry(moves[index])); - } + Widget list() => ListView.builder( + itemCount: moves.length, + itemBuilder: (ctx, index) => moveEntry(moves[index])); @override - Widget build(BuildContext context) { - return Container( - child: list(), - ); - } + Widget build(BuildContext context) => Container(child: list()); } diff --git a/packages/mobility_features/example/lib/places_page.dart b/packages/mobility_features/example/lib/places_page.dart index e1d8f551b..2372756e7 100644 --- a/packages/mobility_features/example/lib/places_page.dart +++ b/packages/mobility_features/example/lib/places_page.dart @@ -1,9 +1,9 @@ -part of mobility_app; +part of 'main.dart'; class PlacesPage extends StatelessWidget { final List places; - PlacesPage(this.places); + const PlacesPage(this.places, {super.key}); Widget placeEntry(Place p) { String lat = p.geoLocation!.latitude.toStringAsFixed(4); @@ -11,24 +11,18 @@ class PlacesPage extends StatelessWidget { return Container( padding: const EdgeInsets.all(2), - margin: EdgeInsets.all(3), + margin: const EdgeInsets.all(3), child: ListTile( leading: Text('Place ID ${p.id}'), title: Text('$lat, $lon'), - trailing: Text('${formatDuration(p.duration)}'), + trailing: Text(formatDuration(p.duration)), )); } - Widget list() { - return ListView.builder( - itemCount: places.length, - itemBuilder: (ctx, index) => placeEntry(places[index])); - } + Widget list() => ListView.builder( + itemCount: places.length, + itemBuilder: (ctx, index) => placeEntry(places[index])); @override - Widget build(BuildContext context) { - return Container( - child: list(), - ); - } + Widget build(BuildContext context) => Container(child: list()); } diff --git a/packages/mobility_features/example/lib/stops_page.dart b/packages/mobility_features/example/lib/stops_page.dart index 66e6bb4fd..8724e8ae7 100644 --- a/packages/mobility_features/example/lib/stops_page.dart +++ b/packages/mobility_features/example/lib/stops_page.dart @@ -1,33 +1,27 @@ -part of mobility_app; +part of 'main.dart'; class StopsPage extends StatelessWidget { final List stops; - StopsPage(this.stops); + const StopsPage(this.stops, {super.key}); Widget stopEntry(Stop s) { String lat = s.geoLocation.latitude.toStringAsFixed(4); String lon = s.geoLocation.longitude.toStringAsFixed(4); return Container( padding: const EdgeInsets.all(2), - margin: EdgeInsets.all(3), + margin: const EdgeInsets.all(3), child: ListTile( leading: Text('Place ${s.placeId}'), - title: Text('${interval(s.arrival, s.departure)}'), + title: Text(interval(s.arrival, s.departure)), trailing: Text('$lat, $lon'), )); } - Widget list() { - return ListView.builder( - itemCount: stops.length, - itemBuilder: (ctx, index) => stopEntry(stops[index])); - } + Widget list() => ListView.builder( + itemCount: stops.length, + itemBuilder: (ctx, index) => stopEntry(stops[index])); @override - Widget build(BuildContext context) { - return Container( - child: list(), - ); - } + Widget build(BuildContext context) => Container(child: list()); } diff --git a/packages/mobility_features/example/pubspec.yaml b/packages/mobility_features/example/pubspec.yaml index 25976b229..d83ac94d1 100644 --- a/packages/mobility_features/example/pubspec.yaml +++ b/packages/mobility_features/example/pubspec.yaml @@ -1,22 +1,20 @@ name: mobility_features_example description: An example app for the Mobility Features package. -publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 3.1.0 +publish_to: "none" environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" dependencies: flutter: sdk: flutter + permission_handler: ^11.3.1 + carp_background_location: ^4.0.0 mobility_features: path: ../ - carp_background_location: - ^4.0.0 - # path: ../../carp_background_location - cupertino_icons: ^1.0.5 dev_dependencies: flutter_test: diff --git a/packages/mobility_features/images/Untitled.drawio b/packages/mobility_features/images/Untitled.drawio deleted file mode 100644 index b26c799ab..000000000 --- a/packages/mobility_features/images/Untitled.drawio +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/packages/mobility_features/images/app.jpeg b/packages/mobility_features/images/app.jpeg new file mode 100644 index 000000000..64550c5fc Binary files /dev/null and b/packages/mobility_features/images/app.jpeg differ diff --git a/packages/mobility_features/images/features.jpeg b/packages/mobility_features/images/features.jpeg index 10611ab12..16b27d506 100644 Binary files a/packages/mobility_features/images/features.jpeg and b/packages/mobility_features/images/features.jpeg differ diff --git a/packages/mobility_features/images/moves.jpeg b/packages/mobility_features/images/moves.jpeg index 355952c2b..d41699842 100644 Binary files a/packages/mobility_features/images/moves.jpeg and b/packages/mobility_features/images/moves.jpeg differ diff --git a/packages/mobility_features/images/places.jpeg b/packages/mobility_features/images/places.jpeg index b2be1bcfa..e6fbfff97 100644 Binary files a/packages/mobility_features/images/places.jpeg and b/packages/mobility_features/images/places.jpeg differ diff --git a/packages/mobility_features/images/stops.jpeg b/packages/mobility_features/images/stops.jpeg index e7a8246db..faacfed49 100644 Binary files a/packages/mobility_features/images/stops.jpeg and b/packages/mobility_features/images/stops.jpeg differ diff --git a/packages/mobility_features/lib/mobility_features.dart b/packages/mobility_features/lib/mobility_features.dart index 7dc0eea0c..736b93f35 100644 --- a/packages/mobility_features/lib/mobility_features.dart +++ b/packages/mobility_features/lib/mobility_features.dart @@ -2,17 +2,20 @@ library mobility_features; import 'dart:async'; import 'dart:math'; -import 'package:simple_cluster/simple_cluster.dart'; -import 'package:stats/stats.dart'; import 'dart:core'; import 'dart:convert'; import 'dart:io'; + +import 'package:stats/stats.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:simple_cluster/simple_cluster.dart'; +import 'package:carp_serializable/carp_serializable.dart'; -part 'package:mobility_features/src/mobility_functions.dart'; -part 'package:mobility_features/src/mobility_domain.dart'; -part 'package:mobility_features/src/mobility_intermediate.dart'; -part 'package:mobility_features/src/mobility_context.dart'; -part 'package:mobility_features/src/mobility_serializer.dart'; -part 'package:mobility_features/src/mobility_features.dart'; -part 'package:mobility_features/src/mobility_file_util.dart'; +part 'src/mobility_context.dart'; +part 'src/domain.dart'; +part 'src/main.dart'; +part 'src/util.dart'; +part 'src/mobility_functions.dart'; +part 'src/serializer.dart'; +part 'mobility_features.g.dart'; diff --git a/packages/mobility_features/lib/mobility_features.g.dart b/packages/mobility_features/lib/mobility_features.g.dart new file mode 100644 index 000000000..376646864 --- /dev/null +++ b/packages/mobility_features/lib/mobility_features.g.dart @@ -0,0 +1,155 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mobility_features.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MobilityContext _$MobilityContextFromJson(Map json) => + MobilityContext() + ..timestamp = json['timestamp'] == null + ? null + : DateTime.parse(json['timestamp'] as String) + ..date = + json['date'] == null ? null : DateTime.parse(json['date'] as String) + ..numberOfStops = (json['numberOfStops'] as num?)?.toInt() + ..numberOfMoves = (json['numberOfMoves'] as num?)?.toInt() + ..numberOfSignificantPlaces = + (json['numberOfSignificantPlaces'] as num?)?.toInt() + ..locationVariance = (json['locationVariance'] as num?)?.toDouble() + ..entropy = (json['entropy'] as num?)?.toDouble() + ..normalizedEntropy = (json['normalizedEntropy'] as num?)?.toDouble() + ..homeStay = (json['homeStay'] as num?)?.toDouble() + ..distanceTraveled = (json['distanceTraveled'] as num?)?.toDouble(); + +Map _$MobilityContextToJson(MobilityContext instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('timestamp', instance.timestamp?.toIso8601String()); + writeNotNull('date', instance.date?.toIso8601String()); + writeNotNull('numberOfStops', instance.numberOfStops); + writeNotNull('numberOfMoves', instance.numberOfMoves); + writeNotNull('numberOfSignificantPlaces', instance.numberOfSignificantPlaces); + writeNotNull('locationVariance', instance.locationVariance); + writeNotNull('entropy', instance.entropy); + writeNotNull('normalizedEntropy', instance.normalizedEntropy); + writeNotNull('homeStay', instance.homeStay); + writeNotNull('distanceTraveled', instance.distanceTraveled); + return val; +} + +GeoLocation _$GeoLocationFromJson(Map json) => GeoLocation( + (json['latitude'] as num).toDouble(), + (json['longitude'] as num).toDouble(), + )..$type = json['__type'] as String?; + +Map _$GeoLocationToJson(GeoLocation instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__type', instance.$type); + val['latitude'] = instance.latitude; + val['longitude'] = instance.longitude; + return val; +} + +LocationSample _$LocationSampleFromJson(Map json) => + LocationSample( + GeoLocation.fromJson(json['geoLocation'] as Map), + DateTime.parse(json['dateTime'] as String), + )..$type = json['__type'] as String?; + +Map _$LocationSampleToJson(LocationSample instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__type', instance.$type); + val['dateTime'] = instance.dateTime.toIso8601String(); + val['geoLocation'] = instance.geoLocation.toJson(); + return val; +} + +Stop _$StopFromJson(Map json) => Stop( + GeoLocation.fromJson(json['geoLocation'] as Map), + DateTime.parse(json['arrival'] as String), + DateTime.parse(json['departure'] as String), + (json['placeId'] as num?)?.toInt() ?? -1, + )..$type = json['__type'] as String?; + +Map _$StopToJson(Stop instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__type', instance.$type); + val['geoLocation'] = instance.geoLocation.toJson(); + val['placeId'] = instance.placeId; + val['arrival'] = instance.arrival.toIso8601String(); + val['departure'] = instance.departure.toIso8601String(); + return val; +} + +Place _$PlaceFromJson(Map json) => Place( + (json['id'] as num).toInt(), + (json['stops'] as List) + .map((e) => Stop.fromJson(e as Map)) + .toList(), + )..$type = json['__type'] as String?; + +Map _$PlaceToJson(Place instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__type', instance.$type); + val['id'] = instance.id; + val['stops'] = instance.stops.map((e) => e.toJson()).toList(); + return val; +} + +Move _$MoveFromJson(Map json) => Move( + Stop.fromJson(json['stopFrom'] as Map), + Stop.fromJson(json['stopTo'] as Map), + (json['distance'] as num?)?.toDouble(), + )..$type = json['__type'] as String?; + +Map _$MoveToJson(Move instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('__type', instance.$type); + val['stopFrom'] = instance.stopFrom.toJson(); + val['stopTo'] = instance.stopTo.toJson(); + writeNotNull('distance', instance.distance); + return val; +} diff --git a/packages/mobility_features/lib/src/domain.dart b/packages/mobility_features/lib/src/domain.dart new file mode 100644 index 000000000..90b83401c --- /dev/null +++ b/packages/mobility_features/lib/src/domain.dart @@ -0,0 +1,372 @@ +part of '../mobility_features.dart'; + +const int HOURS_IN_A_DAY = 24; + +/// Interface representing a geo location. +abstract interface class GeoSpatial { + GeoLocation get geoLocation; +} + +/// Interface for timestamped entities. +abstract interface class Timestamped { + DateTime get dateTime; +} + +/// Utility class for calculating distances. +class Distance { + static double fromGeoSpatial(GeoSpatial a, GeoSpatial b) { + return fromList([a.geoLocation.latitude, a.geoLocation.longitude], + [b.geoLocation.latitude, b.geoLocation.longitude]); + } + + static double fromList(List p1, List p2) { + double lat1 = p1[0]!.radiansFromDegrees; + double lon1 = p1[1]!.radiansFromDegrees; + double lat2 = p2[0]!.radiansFromDegrees; + double lon2 = p2[1]!.radiansFromDegrees; + double earthRadius = 6378137.0; // WGS84 major axis + double distance = 2 * + earthRadius * + asin(sqrt(pow(sin(lat2 - lat1) / 2, 2) + + cos(lat1) * cos(lat2) * pow(sin(lon2 - lon1) / 2, 2))); + + return distance; + } +} + +/// A [GeoLocation] object contains a latitude and longitude +/// and represents a 2D spatial coordinates +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class GeoLocation extends Serializable implements GeoSpatial { + double latitude, longitude; + + GeoLocation(this.latitude, this.longitude); + + @override + GeoLocation get geoLocation => this; + + @override + Function get fromJsonFunction => _$GeoLocationFromJson; + factory GeoLocation.fromJson(Map json) => + FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$GeoLocationToJson(this); + + @override + String toString() => '($latitude, $longitude)'; +} + +/// A [LocationSample] holds a 2D [GeoLocation] spatial data point +/// as well as a [DateTime] value s.t. it may be temporally ordered +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class LocationSample extends Serializable implements GeoSpatial, Timestamped { + @override + DateTime dateTime; + @override + GeoLocation geoLocation; + + LocationSample(this.geoLocation, this.dateTime); + + double? get latitude => geoLocation.latitude; + double? get longitude => geoLocation.longitude; + + LocationSample addNoise() { + double lat = geoLocation.latitude * 1.000001; + double lon = geoLocation.longitude * 1.000001; + return LocationSample(GeoLocation(lat, lon), dateTime); + } + + @override + Function get fromJsonFunction => _$LocationSampleFromJson; + factory LocationSample.fromJson(Map json) => + FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$LocationSampleToJson(this); + + @override + String toString() => '($latitude, $longitude) @ $dateTime'; +} + +/// A [Stop] represents a cluster of [LocationSample] which were 'close' to each other +/// wrt. to Time and 2D space, in a period of little- to no movement. +/// A [Stop] has an assigned [placeId] which links it to a [Place]. +/// At initialization a stop will be assigned to the 'Noise' place (with id -1), +/// and only after all places have been identified will a [Place] be assigned. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class Stop extends Serializable implements GeoSpatial, Timestamped { + @override + GeoLocation geoLocation; + int placeId = -1; + DateTime arrival, departure; + + Stop(this.geoLocation, this.arrival, this.departure, [int placeId = -1]); + + /// Construct stop from point cloud + factory Stop.fromLocationSamples(List locationSamples, + [int placeId = -1]) { + // Calculate center + GeoLocation center = _computeCentroid(locationSamples); + return Stop(center, locationSamples.first.dateTime, + locationSamples.last.dateTime, placeId); + } + + @override + DateTime get dateTime => arrival; + + List get hourSlots { + int startHour = arrival.hour; + int endHour = departure.hour; + + List hours = List.filled(HOURS_IN_A_DAY, 0.0); + + // Start and end should be on the same date! + if (departure.midnight == arrival.midnight) { + // If arrived and departed within same hour + if (startHour == endHour) { + hours[startHour] = (departure.minute - arrival.minute) / 60.0; + } + + // Otherwise if the stop has overlap in hours + else { + // Start + hours[startHour] = 1.0 - arrival.minute / 60.0; + + // In between + for (int hour = startHour + 1; hour < endHour; hour++) { + hours[hour] = 1.0; + } + + // Departure + hours[endHour] = departure.minute / 60.0; + } + } + return hours; + } + + Duration get duration => Duration( + milliseconds: + departure.millisecondsSinceEpoch - arrival.millisecondsSinceEpoch); + + @override + Function get fromJsonFunction => _$StopFromJson; + factory Stop.fromJson(Map json) => + FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$StopToJson(this); + + @override + String toString() => + 'Stop at place $placeId, (${geoLocation.toString()}) [$arrival - $departure] (Duration: $duration) '; +} + +/// A [Place] is a cluster of [Stop]s found by the DBSCAN algorithm +/// https://www.aaai.org/Papers/KDD/1996/KDD96-037.pdf +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class Place extends Serializable { + final int id; + final List stops; + GeoLocation? _geoLocation; + + Place(this.id, this.stops); + + Duration get duration => stops.map((s) => s.duration).reduce((a, b) => a + b); + + Duration durationForDate(DateTime? d) => stops + .where((s) => s.arrival.midnight == d) + .map((s) => s.duration) + .fold(const Duration(), (a, b) => a + b); + + GeoLocation? get geoLocation => _geoLocation ??= _computeCentroid(stops); + + @override + Function get fromJsonFunction => _$PlaceFromJson; + factory Place.fromJson(Map json) => + FromJsonFactory().fromJson(json); + @override + Map toJson() => _$PlaceToJson(this); + + @override + String toString() => + 'Place ID: $id, at ${geoLocation.toString()} ($duration)'; +} + +/// A [Move] is a transfer from one [Stop] to another. +/// A set of features can be derived from this such as the haversine distance between +/// the stops, the duration of the move, and thereby also the average travel speed. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class Move extends Serializable implements Timestamped { + Stop stopFrom, stopTo; + + /// The haversine distance through all the samples between the two stops + double? distance; + + Move(this.stopFrom, this.stopTo, [this.distance]); + + /// Create a Move with a path of samples between two stops + factory Move.fromPath(Stop a, Stop b, List path) { + double d = 0.0; + for (int i = 0; i < path.length - 1; i++) { + d += Distance.fromGeoSpatial(path[i], path[i + 1]); + } + return Move(a, b, d); + } + + /// Create a Move with a straight line between two stops + // ignore: unused_element + factory Move.fromStops(Stop a, Stop b, {double? distance}) { + /// Distance can be overridden. If it was not then it should be computed + distance ??= Distance.fromGeoSpatial(a, b); + return Move(a, b, distance); + } + + /// The duration of the move in milliseconds + Duration get duration => Duration( + milliseconds: stopTo.arrival.millisecondsSinceEpoch - + stopFrom.departure.millisecondsSinceEpoch); + + /// The average speed when moving between the two places (m/s) + double get meanSpeed => distance! / duration.inSeconds.toDouble(); + + int? get placeFrom => stopFrom.placeId; + + int? get placeTo => stopTo.placeId; + + @override + DateTime get dateTime => stopFrom.arrival; + + @override + Function get fromJsonFunction => _$MoveFromJson; + factory Move.fromJson(Map json) => + FromJsonFactory().fromJson(json); + + @override + Map toJson() => _$MoveToJson(this); + + @override + String toString() => + 'Move (Place ${stopFrom.placeId} [${stopFrom.dateTime}] -> Place ${stopTo.placeId} [${stopTo.dateTime}]) ($duration) (${distance!.toInt()} meters)'; +} + +class HourMatrix { + List> matrix; + late int _numberOfPlaces; + + int get numberOfPlaces => _numberOfPlaces; + + HourMatrix(this.matrix) { + _numberOfPlaces = matrix.first.length; + } + + factory HourMatrix.fromStops(List stops, int numPlaces) { + // Init 2d matrix with 24 rows and cols equal to number of places + List> matrix = List.generate( + HOURS_IN_A_DAY, (_) => List.filled(numPlaces, 0.0)); + + for (int j = 0; j < numPlaces; j++) { + List stopsAtPlace = stops.where((s) => (s.placeId) == j).toList(); + + for (Stop s in stopsAtPlace) { + // For each hour of the day, add the hours from the StopRow to the matrix + for (int i = 0; i < HOURS_IN_A_DAY; i++) { + matrix[i][j] += s.hourSlots[i]; + } + } + } + return HourMatrix(matrix); + } + + // ignore: unused_element + factory HourMatrix.routineMatrix(List matrices) { + int nDays = matrices.length; + int nPlaces = matrices.first.matrix.first.length; + List> avg = zeroMatrix(HOURS_IN_A_DAY, nPlaces); + + for (HourMatrix m in matrices) { + for (int i = 0; i < HOURS_IN_A_DAY; i++) { + for (int j = 0; j < nPlaces; j++) { + avg[i][j] += m.matrix[i][j] / nDays; + } + } + } + return HourMatrix(avg); + } + + /// Features + int get homePlaceId { + int startHour = 0, endHour = 6; + + List hourSpentAtPlace = List.filled(numberOfPlaces, 0.0); + + for (int placeId = 0; placeId < numberOfPlaces; placeId++) { + for (int hour = startHour; hour < endHour; hour++) { + hourSpentAtPlace[placeId] += matrix[hour][placeId]; + } + } + double timeSpentAtNight = hourSpentAtPlace.fold(0.0, (a, b) => a + b); + if (timeSpentAtNight > 0) { + return argmaxDouble(hourSpentAtPlace); + } + return -1; + } + + double get sum { + double s = 0.0; + for (int i = 0; i < HOURS_IN_A_DAY; i++) { + for (int j = 0; j < numberOfPlaces; j++) { + s += matrix[i][j]; + } + } + return s; + } + + /// Calculates the error between two matrices + double computeOverlap(HourMatrix other) { + /// Check that dimensions match + assert(other.matrix.length == HOURS_IN_A_DAY && + other.matrix.first.length == matrix.first.length); + + double maxOverlap = min(sum, other.sum); + + if (maxOverlap == 0.0) return -1.0; + + /// Cumulative error between the two matrices + double overlap = 0.0; + // + for (int i = 0; i < HOURS_IN_A_DAY; i++) { + for (int j = 0; j < numberOfPlaces; j++) { + /// If overlap in time-place matrix, + /// add the overlap to the total overlap. + /// The overlap is equal to the minimum of the two quantities + if (matrix[i][j] >= 0.0 && other.matrix[i][j] >= 0.0) { + overlap += min(matrix[i][j], other.matrix[i][j]); + } + } + } + + /// Compute average error by dividing by the number of total entries + return overlap / maxOverlap; + } + + @override + String toString() { + String s = '\n'; + s += 'Home place ID: $homePlaceId\n'; + s += 'Matrix\t\t'; + for (int p = 0; p < numberOfPlaces; p++) { + s += 'Place $p\t\t'; + } + s += '\n'; + for (int hour = 0; hour < HOURS_IN_A_DAY; hour++) { + s += 'Hour ${hour.toString().padLeft(2, '0')}\t\t'; + + for (double e in matrix[hour]) { + s += '${e.toStringAsFixed(3)}\t\t'; + } + s += '\n'; + } + return s; + } +} diff --git a/packages/mobility_features/lib/src/mobility_features.dart b/packages/mobility_features/lib/src/main.dart similarity index 61% rename from packages/mobility_features/lib/src/mobility_features.dart rename to packages/mobility_features/lib/src/main.dart index c0ba592e5..74f10f916 100644 --- a/packages/mobility_features/lib/src/mobility_features.dart +++ b/packages/mobility_features/lib/src/main.dart @@ -1,19 +1,23 @@ -part of mobility_features; +part of '../mobility_features.dart'; /// Main entry for configuring and listening for mobility features. -/// Used as a singleton `MobilityFactory()`. +/// Used as a singleton `MobilityFeatures()`. class MobilityFeatures { + static final MobilityFeatures _instance = MobilityFeatures._(); + double _stopRadius = 5, _placeRadius = 50; Duration _stopDuration = const Duration(seconds: 20); + final _streamController = StreamController.broadcast(); StreamSubscription? _subscription; - _MobilitySerializer? _serializerSamples; - late _MobilitySerializer _serializerStops; - late _MobilitySerializer _serializerMoves; + MobilitySerializer? _serializerSamples; + late MobilitySerializer _serializerStops; + late MobilitySerializer _serializerMoves; List _stops = []; List _moves = []; List _places = []; - List _cluster = [], _buffer = [], _samples = []; + List _cluster = []; + final List _buffer = [], _samples = []; int _saveEvery = 10; bool debug = false; @@ -23,25 +27,31 @@ class MobilityFeatures { } } - // Outgoing stream - StreamController _streamController = - StreamController.broadcast(); - - Stream get contextStream => _streamController.stream; - // Private constructor - MobilityFeatures._(); - - // Private Singleton field - static final MobilityFeatures _instance = MobilityFeatures._(); + MobilityFeatures._() { + FromJsonFactory().registerAll([ + GeoLocation(0, 0), + LocationSample(GeoLocation(0, 0), DateTime.now()), + Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), + Place(0, []), + Move(Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), + Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now())) + ]); + } - /// Public getter for the Singleton instance + /// Singleton instance of MobilityFeatures. factory MobilityFeatures() => _instance; - /// Listen to a Stream of [LocationSample]. - /// The subscription will be stored as a [StreamSubscription] - /// which may be cancelled later. - Future startListening(Stream stream) async { + /// A stream of generated mobility context objects. + Stream get contextStream => _streamController.stream; + + /// Start listening to the [stream] of [LocationSample] updates. + /// This will start calculating [MobilityContext] instances, which will be + /// delivered on the [contextStream] stream. + /// + /// Use [stopListening] to stop listening to the location stream and hence stop + /// generating mobility context objects. + Future startListening(Stream stream) async { await _handleInit(); if (_subscription != null) { @@ -52,9 +62,9 @@ class MobilityFeatures { Future _handleInit() async { _serializerSamples = - _serializerSamples = _MobilitySerializer(); - _serializerStops = _MobilitySerializer(); - _serializerMoves = _MobilitySerializer(); + _serializerSamples = MobilitySerializer(); + _serializerStops = MobilitySerializer(); + _serializerMoves = MobilitySerializer(); _stops = (await _serializerStops.load() as List); _moves = (await _serializerMoves.load() as List); @@ -62,8 +72,9 @@ class MobilityFeatures { _stops = uniqueElements(_stops) as List; _moves = uniqueElements(_moves) as List; - if (_cluster.isNotEmpty) + if (_cluster.isNotEmpty) { _print('Loaded ${_cluster.length} location samples from disk'); + } if (_stops.isNotEmpty) { _print('Loaded ${_stops.length} stops from disk'); } @@ -71,24 +82,26 @@ class MobilityFeatures { _print('Loaded ${_moves.length} moves from disk'); } - for (var s in _stops) _print(s); - if (_stops.isNotEmpty) { /// Only keeps stops and moves from the last known date - DateTime date = _stops.last.datetime.midnight; + DateTime date = _stops.last.dateTime.midnight; _stops = _getElementsForDate(_stops, date) as List; _moves = _getElementsForDate(_moves, date) as List; _places = _findPlaces(_stops, placeRadius: _placeRadius); // Compute features - MobilityContext context = - MobilityContext._(_stops, _places, _moves, date); + MobilityContext context = MobilityContext.fromMobility( + date, + _stops, + _places, + _moves, + ); _streamController.add(context); } } /// Cancel the [StreamSubscription] and stop listening. - Future stopListening() async { + Future stopListening() async { if (_subscription != null) { await _subscription!.cancel(); } @@ -111,7 +124,7 @@ class MobilityFeatures { // If previous samples exist, check if we should compute anything if (_cluster.isNotEmpty) { // If previous sample was on a different date, reset everything - if (_cluster.last.datetime.midnight != sample.datetime.midnight) { + if (_cluster.last.dateTime.midnight != sample.dateTime.midnight) { _createStopAndResetCluster(); _clearEverything(); } @@ -122,7 +135,7 @@ class MobilityFeatures { GeoLocation centroid = _computeCentroid(_cluster); // If the new data point is far away from cluster, make stop - if (Distance.fromGeospatial(centroid, sample) > _stopRadius) { + if (Distance.fromGeoSpatial(centroid, sample) > _stopRadius) { _createStopAndResetCluster(); } } @@ -133,28 +146,28 @@ class MobilityFeatures { void _clearEverything() { _print('cleared'); - _serializerStops.flush(); - _serializerMoves.flush(); - _stops = []; - _moves = []; - _places = []; - _cluster = []; - _buffer = []; + _serializerStops.clear(); + _serializerMoves.clear(); + _stops.clear(); + _moves.clear(); + _places.clear(); + _cluster.clear(); + _buffer.clear(); } /// Save a sample to the buffer and store samples on disk if buffer overflows void _addToBuffer(LocationSample sample) { _buffer.add(sample); if (_buffer.length >= _saveEvery) { - _serializerSamples!.save(_buffer); + _serializerSamples!.append(_buffer); _print('Stored buffer to disk'); - _buffer = []; + _buffer.clear(); } } /// Converts the cluster into a stop, i.e. closing the cluster void _createStopAndResetCluster() { - Stop s = Stop._fromLocationSamples(_cluster); + Stop s = Stop.fromLocationSamples(_cluster); // If the stop is too short, it is discarded // Otherwise compute a context and send it via the stream @@ -172,28 +185,32 @@ class MobilityFeatures { _places = _findPlaces(_stops); // Store to disk - _serializerStops.flush(); - _serializerStops.save(_stops); + _serializerStops.clear(); + _serializerStops.append(_stops); // Extract date - DateTime date = _cluster.last.datetime.midnight; + DateTime date = _cluster.last.dateTime.midnight; if (stopPrev != null) { _moves = _findMoves(_stops, _samples); - _serializerMoves.flush(); - _serializerMoves.save(_moves); + _serializerMoves.clear(); + _serializerMoves.append(_moves); } // Compute features - MobilityContext context = - MobilityContext._(_stops, _places, _moves, date); + MobilityContext context = MobilityContext.fromMobility( + date, + _stops, + _places, + _moves, + ); _streamController.add(context); } // Reset samples etc - _cluster = []; - _serializerSamples!.flush(); - _buffer = []; + _cluster.clear(); + _serializerSamples!.clear(); + _buffer.clear(); } /// Configure the stop-duration for the stop algorithm @@ -207,21 +224,17 @@ class MobilityFeatures { } /// Configure the stop-radius for the place algorithm - set placeRadius(value) { - _placeRadius = value; - } + set placeRadius(double value) => _placeRadius = value; - Future<_MobilitySerializer> + Future> get _locationSampleSerializer async { - if (_serializerSamples == null) { - _serializerSamples = _MobilitySerializer(); - } + _serializerSamples ??= MobilitySerializer(); return _serializerSamples!; } Future saveSamples(List samples) async { final serializer = await _locationSampleSerializer; - serializer.save(samples); + serializer.append(samples); } Future> loadSamples() async { @@ -229,18 +242,18 @@ class MobilityFeatures { return (await serializer.load() as List); } - static List<_Timestamped> _getElementsForDate( - List<_Timestamped> elements, DateTime date) { - return elements.where((e) => e.datetime.midnight == date).toList(); + static List _getElementsForDate( + List elements, DateTime date) { + return elements.where((e) => e.dateTime.midnight == date).toList(); } - static List<_Timestamped> uniqueElements(List<_Timestamped> elements) { + static List uniqueElements(List elements) { List seen = []; - elements.sort((a, b) => a.datetime.compareTo(b.datetime)); + elements.sort((a, b) => a.dateTime.compareTo(b.dateTime)); return elements.where((e) { - int ms = e.datetime.millisecondsSinceEpoch; + int ms = e.dateTime.millisecondsSinceEpoch; if (!seen.contains(ms)) { seen.add(ms); return true; diff --git a/packages/mobility_features/lib/src/mobility_context.dart b/packages/mobility_features/lib/src/mobility_context.dart index 5cef28c89..9a8cd4af0 100644 --- a/packages/mobility_features/lib/src/mobility_context.dart +++ b/packages/mobility_features/lib/src/mobility_context.dart @@ -1,140 +1,147 @@ -part of mobility_features; +part of '../mobility_features.dart'; -/// Daily mobility context. +/// Daily mobility context for a day on [date]. /// -/// All Stops and Moves are on the same [date]. -/// [places] are all places for which the duration on the given date is greater than 0. +/// All [stops] and [moves] are on the same [date]. +/// The [places] are all places for which the duration on the given [date] +/// is greater than 0. +@JsonSerializable(includeIfNull: false, explicitToJson: true) class MobilityContext { - late DateTime _timestamp, _date; - List _stops; - List _places; - List _moves; - late List _significantPlaces; + List? _significantPlaces; + HourMatrix? _hourMatrix; Place? _homePlace; + List? _stops; + List? _places; + List? _moves; - late _HourMatrix _hourMatrix; + /// Timestamp at which the features were computed + DateTime? timestamp; - double? _locationVariance, - _entropy, - _normalizedEntropy, - _homeStay, - _distanceTravelled; - List? contexts; + /// The date of this context. + DateTime? date; - /// Private constructor, cannot be instantiated from outside - MobilityContext._(this._stops, this._places, this._moves, this._date) { - _timestamp = DateTime.now(); + /// Number of stops made today. + int? numberOfStops; - // if contexts array is null, init to empty array - contexts = contexts ?? []; + /// Number of moves made today. + int? numberOfMoves; - // compute all the features - _significantPlaces = - _places.where((p) => p.duration > Duration(minutes: 3)).toList(); - _hourMatrix = _HourMatrix.fromStops(_stops, _places.length); - _homePlace = _findHomePlaceToday(); - _homeStay = _calculateHomeStay(); - _locationVariance = _calculateLocationVariance(); - _entropy = _calculateEntropy(); - _normalizedEntropy = _calculateNormalizedEntropy(); - _distanceTravelled = _calculateDistanceTravelled(); - } + /// Number of significant places visited today. + int? numberOfSignificantPlaces; - // The date of this context. - DateTime get date => _date; + /// Location variance today. + double? locationVariance; - /// Timestamp at which the features were computed - DateTime get timestamp => _timestamp; + /// Location entropy. + /// + /// * High entropy: Time is spent evenly among all places + /// * Low entropy: Time is mainly spent at a few of the places + double? entropy; - /// Stops today. - List get stops => _stops; + /// Normalized location entropy. A scalar between 0 and 1. + double? normalizedEntropy; - /// Moves today. - List get moves => _moves; + /// Home Stay Percentage today. A scalar between 0 and 1. + /// Returns null if cannot be calculated based on the available data. + double? homeStay; - /// Places today. - List get places => _places; + /// Distance traveled today in meters. + double? distanceTraveled; - /// All significant places, i.e. places with a minimum stay duration. - List get significantPlaces => _significantPlaces; + MobilityContext(); - /// Number of significant places visited today. - int get numberOfSignificantPlaces => _significantPlaces.length; + MobilityContext.fromMobility( + this.date, + this._stops, + this._places, + this._moves, + ) { + timestamp = DateTime.now(); - /// Home place. - /// Returns null if home cannot be found from the available data. - Place? get homePlace => _homePlace; + // compute all features + _significantPlaces = + _places!.where((p) => p.duration > const Duration(minutes: 3)).toList(); + _hourMatrix = HourMatrix.fromStops(_stops!, _places!.length); + numberOfSignificantPlaces = _significantPlaces!.length; + numberOfStops = _stops?.length; + numberOfMoves = _moves?.length; + _homePlace = _findHomePlaceToday(); + homeStay = _calculateHomeStay(); + locationVariance = _calculateLocationVariance(); + entropy = _calculateEntropy(); + normalizedEntropy = _calculateNormalizedEntropy(); + distanceTraveled = _calculateDistanceTraveled(); + } - /// Home Stay Percentage today. A scalar between 0 and 1. - /// Returns null if cannot be calculated based on the available data. - double? get homeStay => _homeStay; + /// Stops today. + List? get stops => _stops; - /// Location variance today. - double? get locationVariance => _locationVariance; + /// Places today. + List? get places => _places; - /// Location entropy. - /// - /// * High entropy: Time is spent evenly among all places - /// * Low entropy: Time is mainly spent at a few of the places - double? get entropy => _entropy; + /// Moves today. + List? get moves => _moves; - /// Normalized location entropy. A scalar between 0 and 1. - double? get normalizedEntropy => _normalizedEntropy; + /// The place used as 'home'. + /// Returns null if home cannot be found from the available data. + Place? get homePlace => _homePlace; - /// Distance travelled today in meters. - double? get distanceTravelled => _distanceTravelled; + /// All significant places, i.e. places with a minimum stay duration. + List? get significantPlaces => _significantPlaces; - /// Private home stay calculation double? _calculateHomeStay() { - if (stops.isEmpty) return null; + if (_stops == null) return null; + if (_stops!.isEmpty) return null; // Latest known sample time - DateTime latestTime = _stops.last.departure; + final latestTime = _stops!.last.departure; // Total time elapsed from midnight until the last stop int totalTime = latestTime.millisecondsSinceEpoch - latestTime.midnight.millisecondsSinceEpoch; // Find todays home id, if no home exists today return null - if (_hourMatrix.homePlaceId == -1) return null; + if (_hourMatrix!.homePlaceId == -1) return null; - int homeTime = stops - .where((s) => s.placeId == _hourMatrix.homePlaceId) + int homeTime = _stops! + .where((s) => s.placeId == _hourMatrix!.homePlaceId) .map((s) => s.duration.inMilliseconds) .fold(0, (a, b) => a + b); return homeTime.toDouble() / totalTime.toDouble(); } - Place? _findHomePlaceToday() => (_hourMatrix.homePlaceId == -1) + Place? _findHomePlaceToday() => (_hourMatrix!.homePlaceId == -1) ? null - : _places.where((p) => p.id == _hourMatrix.homePlaceId).first; + : _places!.where((p) => p.id == _hourMatrix!.homePlaceId).first; - /// Location variance calculation double? _calculateLocationVariance() { // Require at least 2 observations - if (_stops.length < 2) return 0.0; + if (_stops!.length < 2) return 0.0; - double latStd = Stats.fromData(_stops.map((s) => (s.geoLocation.latitude))) + double latStd = Stats.fromData(_stops!.map((s) => (s.geoLocation.latitude))) .standardDeviation as double; - double lonStd = Stats.fromData(_stops.map((s) => (s.geoLocation.longitude))) - .standardDeviation as double; + double lonStd = + Stats.fromData(_stops!.map((s) => (s.geoLocation.longitude))) + .standardDeviation as double; return log(latStd * latStd + lonStd * lonStd + 1); } double? _calculateEntropy() { // if no places were visited return null - if (places.isEmpty) + // else - the Entropy is zero when one outcome is certain to occur + if (_places!.isEmpty) { return null; - // the Entropy is zero when one outcome is certain to occur - else if (places.length == 1) return 0.0; + } else if (_places!.length == 1) { + return 0.0; + } // calculate time spent at different places List durations = - places.map((p) => p.durationForDate(date)).toList(); + _places!.map((p) => p.durationForDate(date)).toList(); - Duration totalTimeSpent = durations.fold(Duration(), (a, b) => a + b); + Duration totalTimeSpent = durations.fold(const Duration(), (a, b) => a + b); List distribution = durations .map((d) => (d.inMilliseconds.toDouble() / @@ -144,25 +151,15 @@ class MobilityContext { return -distribution.map((p) => p * log(p)).reduce((a, b) => (a + b)); } - /// Private normalized entropy calculation double _calculateNormalizedEntropy() => - (places.length == 1) ? 0.0 : entropy! / log(places.length); - - /// Private distance travelled calculation - double _calculateDistanceTravelled() => - _moves.map((m) => (m.distance)).fold(0.0, (a, b) => a + b!); - - Map toJson() => { - "date": date.toIso8601String(), - "computed_at": timestamp.toIso8601String(), - "num_of_stops": stops.length, - "num_of_moves": moves.length, - "num_of_significant_places": significantPlaces.length, - "normalized_entropy": normalizedEntropy, - "home_stay": homeStay, - "distance_travelled": distanceTravelled, - "location_variance": locationVariance - }; + (_places!.length == 1) ? 0.0 : entropy! / log(_places!.length); + + double _calculateDistanceTraveled() => + _moves!.map((m) => (m.distance)).fold(0.0, (a, b) => a + b!); + + factory MobilityContext.fromJson(Map json) => + _$MobilityContextFromJson(json); + Map toJson() => _$MobilityContextToJson(this); } // /// Routine index (overlap) calculation diff --git a/packages/mobility_features/lib/src/mobility_domain.dart b/packages/mobility_features/lib/src/mobility_domain.dart deleted file mode 100644 index b48fa1b5d..000000000 --- a/packages/mobility_features/lib/src/mobility_domain.dart +++ /dev/null @@ -1,423 +0,0 @@ -part of mobility_features; - -const int HOURS_IN_A_DAY = 24; -const String _LATITUDE = 'latitude', - _LONGITUDE = 'longitude', - _DATETIME = 'datetime', - _ARRIVAL = 'arrival', - _DEPARTURE = 'departure', - _PLACE_ID = 'place_id', - _STOP_FROM = 'stop_from', - _STOP_TO = 'stop_to', - _DISTANCE = 'distance', - _GEO_LOCATION = 'geo_location'; - -/// Abstract class to enforce functions -/// to serialize and deserialize an object -abstract class _Serializable { - Map toJson(); - _Serializable._fromJson(Map json); -} - -/// Simple abstract class to let the compiler know that an object -/// implementing this class has a location -abstract class _Geospatial { - GeoLocation get geoLocation; -} - -abstract class _Timestamped { - DateTime get datetime; -} - -class Distance { - static double fromGeospatial(_Geospatial a, _Geospatial b) { - return fromList([a.geoLocation.latitude, a.geoLocation.longitude], - [b.geoLocation.latitude, b.geoLocation.longitude]); - } - - static double fromList(List p1, List p2) { - double lat1 = p1[0]!.radiansFromDegrees; - double lon1 = p1[1]!.radiansFromDegrees; - double lat2 = p2[0]!.radiansFromDegrees; - double lon2 = p2[1]!.radiansFromDegrees; - double earthRadius = 6378137.0; // WGS84 major axis - double distance = 2 * - earthRadius * - asin(sqrt(pow(sin(lat2 - lat1) / 2, 2) + - cos(lat1) * cos(lat2) * pow(sin(lon2 - lon1) / 2, 2))); - - return distance; - } -} - -/// A [GeoLocation] object contains a latitude and longitude -/// and represents a 2D spatial coordinates -class GeoLocation implements _Serializable, _Geospatial { - late double _latitude; - late double _longitude; - - GeoLocation(this._latitude, this._longitude); - - factory GeoLocation.fromJson(Map x) { - num? lat = x[_LATITUDE] as double?; - num? lon = x[_LONGITUDE] as double?; - return GeoLocation(lat as double, lon as double); - } - - double get latitude => _latitude; - - double get longitude => _longitude; - - GeoLocation get geoLocation => this; - - Map toJson() => {_LATITUDE: latitude, _LONGITUDE: longitude}; - - @override - String toString() => '($_latitude, $_longitude)'; -} - -/// A [LocationSample] holds a 2D [GeoLocation] spatial data point -/// as well as a [DateTime] value s.t. it may be temporally ordered -class LocationSample implements _Serializable, _Geospatial, _Timestamped { - DateTime _datetime; - GeoLocation _geoLocation; - - LocationSample(this._geoLocation, this._datetime); - - double? get latitude => geoLocation.latitude; - - double? get longitude => geoLocation.longitude; - - DateTime get datetime => _datetime; - - GeoLocation get geoLocation => _geoLocation; - - Map toJson() => { - _GEO_LOCATION: geoLocation.toJson(), - _DATETIME: json.encode(datetime.millisecondsSinceEpoch) - }; - - factory LocationSample._fromJson(Map json) { - /// Parse, i.e. perform type check - GeoLocation pos = GeoLocation.fromJson(json[_GEO_LOCATION]); - int millis = int.parse(json[_DATETIME]); - DateTime dt = DateTime.fromMillisecondsSinceEpoch(millis); - return LocationSample(pos, dt); - } - - LocationSample addNoise() { - double lat = this.geoLocation.latitude * 1.000001; - double lon = this.geoLocation.longitude * 1.000001; - return LocationSample(GeoLocation(lat, lon), this.datetime); - } - - @override - String toString() { - return '($latitude, $longitude) @ $_datetime'; - } -} - -/// A [Stop] represents a cluster of [LocationSample] which were 'close' to eachother -/// wrt. to Time and 2D space, in a period of little- to no movement. -/// A [Stop] has an assigned [placeId] which links it to a [Place]. -/// At initialization a stop will be assigned to the 'Noise' place (with id -1), -/// and only after all places have been identified will a [Place] be assigned. -class Stop implements _Serializable, _Geospatial, _Timestamped { - GeoLocation _geoLocation; - int? placeId; - DateTime _arrival, _departure; - - Stop._(this._geoLocation, this._arrival, this._departure, - {this.placeId = -1}); - - /// Construct stop from point cloud - factory Stop._fromLocationSamples(List locationSamples, - {int placeId = -1}) { - // Calculate center - GeoLocation center = _computeCentroid(locationSamples); - return Stop._( - center, locationSamples.first.datetime, locationSamples.last.datetime, - placeId: placeId); - } - - GeoLocation get geoLocation => _geoLocation; - - DateTime get departure => _departure; - - DateTime get arrival => _arrival; - - DateTime get datetime => _arrival; - - List get hourSlots { - int startHour = arrival.hour; - int endHour = departure.hour; - - List hours = List.filled(HOURS_IN_A_DAY, 0.0); - - // Start and end should be on the same date! - if (departure.midnight == arrival.midnight) { - // If arrived and departed within same hour - if (startHour == endHour) { - hours[startHour] = (departure.minute - arrival.minute) / 60.0; - } - - // Otherwise if the stop has overlap in hours - else { - // Start - hours[startHour] = 1.0 - arrival.minute / 60.0; - - // In between - for (int hour = startHour + 1; hour < endHour; hour++) { - hours[hour] = 1.0; - } - - // Departure - hours[endHour] = departure.minute / 60.0; - } - } - return hours; - } - - Duration get duration => Duration( - milliseconds: - departure.millisecondsSinceEpoch - arrival.millisecondsSinceEpoch); - - Map toJson() => { - _GEO_LOCATION: geoLocation.toJson(), - _PLACE_ID: placeId, - _ARRIVAL: arrival.millisecondsSinceEpoch, - _DEPARTURE: departure.millisecondsSinceEpoch - }; - - factory Stop._fromJson(Map json) { - return Stop._( - GeoLocation.fromJson(json[_GEO_LOCATION]), - DateTime.fromMillisecondsSinceEpoch(json[_ARRIVAL]), - DateTime.fromMillisecondsSinceEpoch(json[_DEPARTURE]), - placeId: json[_PLACE_ID]); - } - - @override - String toString() { - return 'Stop at place $placeId, (${_geoLocation.toString()}) [$arrival - $departure] (Duration: $duration) '; - } -} - -/// A [Place] is a cluster of [Stop]s found by the DBSCAN algorithm -/// https://www.aaai.org/Papers/KDD/1996/KDD96-037.pdf -class Place { - int _id; - List _stops; - GeoLocation? _geoLocation; - - Place._(this._id, this._stops); - - Duration get duration => - _stops.map((s) => s.duration).reduce((a, b) => a + b); - - Duration durationForDate(DateTime? d) => _stops - .where((s) => s.arrival.midnight == d) - .map((s) => s.duration) - .fold(Duration(), (a, b) => a + b); - - GeoLocation? get geoLocation { - if (_geoLocation == null) { - _geoLocation = _computeCentroid(_stops); - } - return _geoLocation; - } - - int get id => _id; - - @override - String toString() { - return 'Place ID: $_id, at ${geoLocation.toString()} ($duration)'; - } -} - -/// A [Move] is a transfer from one [Stop] to another. -/// A set of features can be derived from this such as the haversine distance between -/// the stops, the duration of the move, and thereby also the average travel speed. -class Move implements _Serializable, _Timestamped { - Stop _stopFrom, _stopTo; - double? _distance; - - Move._(this._stopFrom, this._stopTo, this._distance); - - /// Create a Move with a path of samples between two stops - factory Move._fromPath(Stop a, Stop b, List path) { - double d = 0.0; - for (int i = 0; i < path.length - 1; i++) { - d += Distance.fromGeospatial(path[i], path[i + 1]); - } - return Move._(a, b, d); - } - - /// Create a Move with a straight line between two stops - // ignore: unused_element - factory Move._fromStops(Stop a, Stop b, {double? distance}) { - /// Distance can be overridden. If it was not then it should be computed - if (distance == null) { - distance = Distance.fromGeospatial(a, b); - } - return Move._(a, b, distance); - } - - /// The haversine distance through all the samples between the two stops - double? get distance => _distance; - - /// The duration of the move in milliseconds - Duration get duration => Duration( - milliseconds: _stopTo.arrival.millisecondsSinceEpoch - - _stopFrom.departure.millisecondsSinceEpoch); - - /// The average speed when moving between the two places (m/s) - double get meanSpeed => distance! / duration.inSeconds.toDouble(); - - int? get placeFrom => _stopFrom.placeId; - - int? get placeTo => _stopTo.placeId; - - Stop get stopFrom => _stopFrom; - - Stop get stopTo => _stopTo; - - DateTime get datetime => stopFrom.arrival; - - Map toJson() => { - _STOP_FROM: _stopFrom.toJson(), - _STOP_TO: _stopTo.toJson(), - _DISTANCE: _distance - }; - - factory Move._fromJson(Map _json) { - return Move._(Stop._fromJson(_json[_STOP_FROM]), - Stop._fromJson(_json[_STOP_TO]), _json[_DISTANCE]); - } - - @override - String toString() { - return 'Move (Place ${_stopFrom.placeId} [${_stopFrom.datetime}] -> Place ${_stopTo.placeId} [${_stopTo.datetime}]) ($duration) (${distance!.toInt()} meters)'; - } -} - -class _HourMatrix { - List> _matrix; - late int _numberOfPlaces; - - _HourMatrix(this._matrix) { - _numberOfPlaces = _matrix.first.length; - } - - factory _HourMatrix.fromStops(List stops, int numPlaces) { - // Init 2d matrix with 24 rows and cols equal to number of places - List> matrix = new List.generate( - HOURS_IN_A_DAY, (_) => new List.filled(numPlaces, 0.0)); - - for (int j = 0; j < numPlaces; j++) { - List stopsAtPlace = stops.where((s) => (s.placeId) == j).toList(); - - for (Stop s in stopsAtPlace) { - // For each hour of the day, add the hours from the StopRow to the matrix - for (int i = 0; i < HOURS_IN_A_DAY; i++) { - matrix[i][j] += s.hourSlots[i]; - } - } - } - return _HourMatrix(matrix); - } - - // ignore: unused_element - factory _HourMatrix.routineMatrix(List<_HourMatrix> matrices) { - int nDays = matrices.length; - int nPlaces = matrices.first.matrix.first.length; - List> avg = zeroMatrix(HOURS_IN_A_DAY, nPlaces); - - for (_HourMatrix m in matrices) { - for (int i = 0; i < HOURS_IN_A_DAY; i++) { - for (int j = 0; j < nPlaces; j++) { - avg[i][j] += m.matrix[i][j] / nDays; - } - } - } - return _HourMatrix(avg); - } - - List> get matrix => _matrix; - - /// Features - int get homePlaceId { - int startHour = 0, endHour = 6; - - List hourSpentAtPlace = List.filled(_numberOfPlaces, 0.0); - - for (int placeId = 0; placeId < _numberOfPlaces; placeId++) { - for (int hour = startHour; hour < endHour; hour++) { - hourSpentAtPlace[placeId] += _matrix[hour][placeId]; - } - } - double timeSpentAtNight = hourSpentAtPlace.fold(0.0, (a, b) => a + b); - if (timeSpentAtNight > 0) { - return argmaxDouble(hourSpentAtPlace); - } - return -1; - } - - double get sum { - double s = 0.0; - for (int i = 0; i < HOURS_IN_A_DAY; i++) { - for (int j = 0; j < _numberOfPlaces; j++) { - s += this.matrix[i][j]; - } - } - return s; - } - - /// Calculates the error between two matrices - double computeOverlap(_HourMatrix other) { - /// Check that dimensions match - assert(other.matrix.length == HOURS_IN_A_DAY && - other.matrix.first.length == _matrix.first.length); - - double maxOverlap = min(this.sum, other.sum); - - if (maxOverlap == 0.0) return -1.0; - - /// Cumulative error between the two matrices - double overlap = 0.0; - // - for (int i = 0; i < HOURS_IN_A_DAY; i++) { - for (int j = 0; j < _numberOfPlaces; j++) { - /// If overlap in time-place matrix, - /// add the overlap to the total overlap. - /// The overlap is equal to the minimum of the two quantities - if (this.matrix[i][j] >= 0.0 && other.matrix[i][j] >= 0.0) { - overlap += min(this.matrix[i][j], other.matrix[i][j]); - } - } - } - - /// Compute average error by dividing by the number of total entries - return overlap / maxOverlap; - } - - @override - String toString() { - String s = '\n'; - s += 'Home place ID: $homePlaceId\n'; - s += 'Matrix\t\t'; - for (int p = 0; p < _numberOfPlaces; p++) { - s += 'Place $p\t\t'; - } - s += '\n'; - for (int hour = 0; hour < HOURS_IN_A_DAY; hour++) { - s += 'Hour ${hour.toString().padLeft(2, '0')}\t\t'; - - for (double e in _matrix[hour]) { - s += '${e.toStringAsFixed(3)}\t\t'; - } - s += '\n'; - } - return s; - } -} diff --git a/packages/mobility_features/lib/src/mobility_file_util.dart b/packages/mobility_features/lib/src/mobility_file_util.dart deleted file mode 100644 index c1c9b4cc0..000000000 --- a/packages/mobility_features/lib/src/mobility_file_util.dart +++ /dev/null @@ -1,42 +0,0 @@ -part of mobility_features; - -const String _LOCATION_SAMPLES_FILE = 'location_samples', - _STOPS_FILE = 'stops', - _MOVES_FILE = 'moves', - _TEST_DATA_PATH = 'test/testdata'; - -Future _fileReference(Type T) async { - bool isMobile = Platform.isAndroid || Platform.isIOS; - - /// If on a mobile device, use the path_provider plugin to access the - /// file system - String path; - if (isMobile) { - path = (await getApplicationDocumentsDirectory()).path; - } - - /// Otherwise if unit testing just use the normal file system - else { - path = _TEST_DATA_PATH; - } - - /// Decide which file to write to, depending on the type (T) - String type = _LOCATION_SAMPLES_FILE; - if (T == Move) { - type = _MOVES_FILE; - } else if (T == Stop) { - type = _STOPS_FILE; - } - - // Create a file reference - File reference = new File('$path/$type.json'); - - // If it does not exist already, - // create it by writing an empty string to it - bool exists = reference.existsSync(); - if (!exists) { - reference.writeAsStringSync('', mode: FileMode.write); - } - - return reference; -} diff --git a/packages/mobility_features/lib/src/mobility_functions.dart b/packages/mobility_features/lib/src/mobility_functions.dart index 7cfb0c566..ba6980f5e 100644 --- a/packages/mobility_features/lib/src/mobility_functions.dart +++ b/packages/mobility_features/lib/src/mobility_functions.dart @@ -1,127 +1,108 @@ -part of mobility_features; +part of '../mobility_features.dart'; -/// Returns an [Iterable] of [List]s where the nth element in the returned -/// iterable contains the nth element from every Iterable in [iterables]. The -/// returned Iterable is as long as the shortest Iterable in the argument. If -/// [iterables] is empty, it returns an empty list. -Iterable> zip(Iterable> iterables) sync* { - if (iterables.isEmpty) return; - final iterators = iterables.map((e) => e.iterator).toList(growable: false); - while (iterators.every((e) => e.moveNext())) { - yield iterators.map((e) => e.current).toList(growable: false); - } -} +/// Finds the places by clustering stops with the DBSCAN algorithm +List _findPlaces(List stops, {double placeRadius = 50.0}) { + List places = []; -/// Convert from degrees to radians -extension on double { - double get radiansFromDegrees => this * (pi / 180.0); -} + DBSCAN dbscan = DBSCAN( + epsilon: placeRadius, minPoints: 1, distanceMeasure: Distance.fromList); -Iterable range(int low, int high) sync* { - for (int i = low; i < high; ++i) { - yield i; - } -} + /// Extract gps coordinates from stops + List> stopCoordinates = stops + .map((s) => ([s.geoLocation.latitude, s.geoLocation.longitude])) + .toList(); -extension CompareDates on DateTime { - bool geq(DateTime other) { - return this.isAfter(other) || this.isAtSameMomentAs(other); - } + /// Run DBSCAN on stops + dbscan.run(stopCoordinates as List>); - bool leq(DateTime other) { - return this.isBefore(other) || this.isAtSameMomentAs(other); - } + /// Extract labels for each stop, each label being a cluster + /// Filter out stops labelled as noise (where label is -1) + Set clusterLabels = dbscan.label!.where((l) => (l != -1)).toSet(); - DateTime get midnight { - return DateTime(this.year, this.month, this.day); - } -} + for (int label in clusterLabels) { + /// Get indices of all stops with the current cluster label + List indices = + stops.asMap().keys.where((i) => (dbscan.label![i] == label)).toList(); -extension AverageIterable on Iterable { - double? get mean { - return this.fold(0, (dynamic a, b) => a + b) / this.length.toDouble(); - } -} + /// For each index, get the corresponding stop + List stopsForPlace = indices.map((i) => (stops[i])).toList(); -int argmaxDouble(List list) { - double maxVal = -double.infinity; - int i = 0; + /// Add place to the list + Place p = Place(label, stopsForPlace); + places.add(p); - for (int j = 0; j < list.length; j++) { - if (list[j] > maxVal) { - maxVal = list[j]; - i = j; + /// Set placeId field for the stops belonging to this place + for (var s in stopsForPlace) { + s.placeId = p.id; } } - return i; + return places; } -int argmaxInt(List list) { - int maxVal = -2147483648; - int i = 0; - - for (int j = 0; j < list.length; j++) { - if (list[j] > maxVal) { - maxVal = list[j]; - i = j; +List _findMoves(List stops, List samples) { + Stop? previous; + List moves = []; + + for (Stop current in stops) { + if (previous != null) { + final path = samples + .where((s) => + previous!.dateTime.leq(s.dateTime) && + previous.dateTime.geq(s.dateTime)) + .toList(); + Move m = Move.fromPath(previous, current, path); + moves.add(m); } + previous = current; } - return i; + return moves; } -void printMatrix(List m) { - for (List row in m) { - String s = ''; - for (var e in row) { - s += '$e '; - } - print(s); - } -} +GeoLocation _computeCentroid(List data) { + double lat = + Stats.fromData(data.map((d) => (d.geoLocation.latitude)).toList()).median + as double; + double lon = + Stats.fromData(data.map((d) => (d.geoLocation.longitude)).toList()).median + as double; -List> zeroMatrix(int rows, int cols) { - return new List.generate(rows, (_) => new List.filled(cols, 0.0)); + return GeoLocation(lat, lon); } -List _mergeStops(List stops) { - List merged = []; - if (stops.length < 2) return stops; - - /// Should be applied after places have been found - List toMerge = []; - - void _merge() { - if (toMerge.isEmpty) return; - GeoLocation geoLocation = _computeCentroid(toMerge); - DateTime arr = toMerge.first.arrival; - DateTime dep = toMerge.last.departure; - Stop s = Stop._(geoLocation, arr, dep, placeId: toMerge.first.placeId); - merged.add(s); - toMerge = []; - } - - for (Stop stop in stops) { - /// If stop is noisy, just add it to the merged list, dont do anything to it - if (stop.placeId == -1) { - merged.add(stop); - } else { - /// If no stops to merge, we cannot merge and we therefore add the current - /// stop and go to the next one - if (toMerge.isEmpty) { - toMerge.add(stop); - } - - /// Otherwise check if we should add it or merge - else { - if (stop.placeId != toMerge.last.placeId) { - _merge(); - } - toMerge.add(stop); - } - } - } - - /// Merge remaining stops in the toMerge list - _merge(); - return merged; -} +/// Find the stops in a sequence of gps data points +//List _findStops(List data, +// {double stopRadius = 25.0, +// Duration stopDuration = const Duration(minutes: 3)}) { +// if (data.isEmpty) return []; +// +// List stops = []; +// int n = data.length; +// +// /// Go through all the location samples, i.e from index [0...n-1] +// int start = 0; +// while (start < n) { +// int end = start + 1; +// List subset = data.sublist(start, end); +// GeoLocation centroid = _computeCentroid(subset); +// +// /// Expand cluster until either all samples have been considered, +// /// or the current sample lies outside the radius. +// while ( +// end < n && Distance.fromGeospatial(centroid, data[end]) <= stopRadius) { +// end += 1; +// subset = data.sublist(start, end); +// centroid = _computeCentroid(subset); +// } +// Stop s = Stop._fromLocationSamples(subset); +// stops.add(s); +// +// /// Update the start index, such that we no longer look at +// /// the previously considered data samples +// start = end; +// } +// +// /// Filter out stops which are shorter than the min. duration +// stops = stops.where((s) => (s.duration >= stopDuration)).toList(); +// +// return stops; +//} diff --git a/packages/mobility_features/lib/src/mobility_intermediate.dart b/packages/mobility_features/lib/src/mobility_intermediate.dart deleted file mode 100644 index 75723927c..000000000 --- a/packages/mobility_features/lib/src/mobility_intermediate.dart +++ /dev/null @@ -1,105 +0,0 @@ -part of mobility_features; - -/// Find the stops in a sequence of gps data points -//List _findStops(List data, -// {double stopRadius = 25.0, -// Duration stopDuration = const Duration(minutes: 3)}) { -// if (data.isEmpty) return []; -// -// List stops = []; -// int n = data.length; -// -// /// Go through all the location samples, i.e from index [0...n-1] -// int start = 0; -// while (start < n) { -// int end = start + 1; -// List subset = data.sublist(start, end); -// GeoLocation centroid = _computeCentroid(subset); -// -// /// Expand cluster until either all samples have been considered, -// /// or the current sample lies outside the radius. -// while ( -// end < n && Distance.fromGeospatial(centroid, data[end]) <= stopRadius) { -// end += 1; -// subset = data.sublist(start, end); -// centroid = _computeCentroid(subset); -// } -// Stop s = Stop._fromLocationSamples(subset); -// stops.add(s); -// -// /// Update the start index, such that we no longer look at -// /// the previously considered data samples -// start = end; -// } -// -// /// Filter out stops which are shorter than the min. duration -// stops = stops.where((s) => (s.duration >= stopDuration)).toList(); -// -// return stops; -//} - -/// Finds the places by clustering stops with the DBSCAN algorithm -List _findPlaces(List stops, {double placeRadius = 50.0}) { - List places = []; - - DBSCAN dbscan = DBSCAN( - epsilon: placeRadius, minPoints: 1, distanceMeasure: Distance.fromList); - - /// Extract gps coordinates from stops - List> stopCoordinates = stops - .map((s) => ([s.geoLocation.latitude, s.geoLocation.longitude])) - .toList(); - - /// Run DBSCAN on stops - dbscan.run(stopCoordinates as List>); - - /// Extract labels for each stop, each label being a cluster - /// Filter out stops labelled as noise (where label is -1) - Set clusterLabels = dbscan.label!.where((l) => (l != -1)).toSet(); - - for (int label in clusterLabels) { - /// Get indices of all stops with the current cluster label - List indices = - stops.asMap().keys.where((i) => (dbscan.label![i] == label)).toList(); - - /// For each index, get the corresponding stop - List stopsForPlace = indices.map((i) => (stops[i])).toList(); - - /// Add place to the list - Place p = Place._(label, stopsForPlace); - places.add(p); - - /// Set placeId field for the stops belonging to this place - stopsForPlace.forEach((s) => s.placeId = p._id); - } - return places; -} - -List _findMoves(List stops, List samples) { - Stop? previous; - List moves = []; - - for (Stop current in stops) { - if (previous != null) { - final path = samples - .where((s) => - previous!.datetime.leq(s.datetime) && - previous.datetime.geq(s.datetime)) - .toList(); - Move m = Move._fromPath(previous, current, path); - moves.add(m); - } - previous = current; - } - return moves; -} - -GeoLocation _computeCentroid(List<_Geospatial> data) { - double lat = - Stats.fromData(data.map((d) => (d.geoLocation.latitude)).toList()).median - as double; - double lon = - Stats.fromData(data.map((d) => (d.geoLocation.longitude)).toList()).median - as double; - return GeoLocation(lat, lon); -} diff --git a/packages/mobility_features/lib/src/mobility_serializer.dart b/packages/mobility_features/lib/src/mobility_serializer.dart deleted file mode 100644 index 826bbc31f..000000000 --- a/packages/mobility_features/lib/src/mobility_serializer.dart +++ /dev/null @@ -1,65 +0,0 @@ -part of mobility_features; - -class _MobilitySerializer { - /// Provide a file reference in order to serialize objects. - File? _file; - String delimiter = '\n'; - - _MobilitySerializer(); - - /// Deletes the content of the file - void flush() { - _file!.writeAsStringSync('', mode: FileMode.write); - } - - Future get file async { - if (_file == null) { - _file = await _fileReference(T); - } - return _file!; - } - - /// Writes a list of [_Serializable] to the file given in the constructor. - void save(List<_Serializable> elements) async { - File f = await file; - String jsonString = ""; - for (_Serializable e in elements) { - jsonString += json.encode(e.toJson()) + delimiter; - } - f.writeAsStringSync(jsonString, mode: FileMode.writeOnlyAppend); - } - - /// Reads contents of the file in the constructor, - /// and maps it to a list of a specific [_Serializable] type. - Future> load() async { - File f = await file; - - /// Read file content as one big string - String content = await f.readAsString(); - - /// Split content into lines by delimiting them - List lines = content.split(delimiter); - - /// Remove last entry since it is always empty - /// Then convert each line to JSON, and then to Dart Map objects - Iterable> jsonObjs = lines - .sublist(0, lines.length - 1) - .map((e) => json.decode(e)) - .map((e) => Map.from(e)); - - switch (T) { - case Move: - - /// Filter out moves which are not recent - return jsonObjs.map((x) => Move._fromJson(x)).toList(); - case Stop: - - /// Filter out stops which are not recent - return jsonObjs.map((x) => Stop._fromJson(x)).toList(); - default: - - /// Filter out data samples not from today - return jsonObjs.map((x) => LocationSample._fromJson(x)).toList(); - } - } -} diff --git a/packages/mobility_features/lib/src/serializer.dart b/packages/mobility_features/lib/src/serializer.dart new file mode 100644 index 000000000..9873f6ba0 --- /dev/null +++ b/packages/mobility_features/lib/src/serializer.dart @@ -0,0 +1,93 @@ +part of '../mobility_features.dart'; + +/// Utility class for (de)serializing [Stop] and [Move] objects. +class MobilitySerializer { + // Provide a file reference in order to serialize objects. + File? _file; + String delimiter = '\n'; + + MobilitySerializer(); + + /// Clears (deletes) the content of [file] + void clear() => _file!.writeAsStringSync('', mode: FileMode.write); + + Future get file async => _file ??= await _fileReference(T); + + /// Appends a list of mobility [elements] to [file]. + void append(List elements) async { + File f = await file; + String jsonString = ""; + for (Serializable element in elements) { + jsonString += json.encode(element.toJson()) + delimiter; + } + f.writeAsStringSync(jsonString, mode: FileMode.writeOnlyAppend); + } + + /// Reads contents of the [file] and maps it to a list of a specific + /// mobility objects. + Future> load() async { + File f = await file; + + // Read file content as one big string + String content = await f.readAsString(); + + // Split content into lines by delimiting them + List lines = content.split(delimiter); + + // Remove last entry since it is always empty + // Then convert each line to JSON, and then to Dart Map objects + Iterable> jsonObjs = lines + .sublist(0, lines.length - 1) + .map((e) => json.decode(e)) + .map((e) => Map.from(e as Map)); + + switch (T) { + // Filter out moves which are not recent + case const (Move): + return jsonObjs.map((x) => Move.fromJson(x)).toList(); + // Filter out stops which are not recent + case const (Stop): + return jsonObjs.map((x) => Stop.fromJson(x)).toList(); + // Filter out data samples not from today + default: + return jsonObjs.map((x) => LocationSample.fromJson(x)).toList(); + } + } +} + +const String _LOCATION_SAMPLES_FILE = 'location_samples', + _STOPS_FILE = 'stops', + _MOVES_FILE = 'moves', + _TEST_DATA_PATH = 'test/testdata'; + +Future _fileReference(Type T) async { + bool isMobile = Platform.isAndroid || Platform.isIOS; + + /// If on a mobile device, use the path_provider plugin to access the + /// file system. Otherwise if unit testing just use the normal file system. + String path; + if (isMobile) { + path = (await getApplicationDocumentsDirectory()).path; + } else { + path = _TEST_DATA_PATH; + } + + /// Decide which file to write to, depending on the type (T) + String type = _LOCATION_SAMPLES_FILE; + if (T == Move) { + type = _MOVES_FILE; + } else if (T == Stop) { + type = _STOPS_FILE; + } + + // Create a file reference + File reference = File('$path/$type.json'); + + // If it does not exist already, create it by writing an empty string to it. + bool exists = reference.existsSync(); + if (!exists) { + reference.writeAsStringSync('', mode: FileMode.write); + } + + return reference; +} diff --git a/packages/mobility_features/lib/src/util.dart b/packages/mobility_features/lib/src/util.dart new file mode 100644 index 000000000..b08b71718 --- /dev/null +++ b/packages/mobility_features/lib/src/util.dart @@ -0,0 +1,111 @@ +part of '../mobility_features.dart'; + +/// Returns an [Iterable] of [List]s where the nth element in the returned +/// iterable contains the nth element from every Iterable in [iterables]. The +/// returned Iterable is as long as the shortest Iterable in the argument. If +/// [iterables] is empty, it returns an empty list. +Iterable> zip(Iterable> iterables) sync* { + if (iterables.isEmpty) return; + final iterators = iterables.map((e) => e.iterator).toList(growable: false); + while (iterators.every((e) => e.moveNext())) { + yield iterators.map((e) => e.current).toList(growable: false); + } +} + +/// Convert from degrees to radians +extension on double { + double get radiansFromDegrees => this * (pi / 180.0); +} + +Iterable range(int low, int high) sync* { + for (int i = low; i < high; ++i) { + yield i; + } +} + +extension CompareDates on DateTime { + bool geq(DateTime other) { + return isAfter(other) || isAtSameMomentAs(other); + } + + bool leq(DateTime other) { + return isBefore(other) || isAtSameMomentAs(other); + } + + DateTime get midnight { + return DateTime(year, month, day); + } +} + +int argmaxDouble(List list) { + double maxVal = -double.infinity; + int i = 0; + + for (int j = 0; j < list.length; j++) { + if (list[j] > maxVal) { + maxVal = list[j]; + i = j; + } + } + return i; +} + +int argmaxInt(List list) { + int maxVal = -2147483648; + int i = 0; + + for (int j = 0; j < list.length; j++) { + if (list[j] > maxVal) { + maxVal = list[j]; + i = j; + } + } + return i; +} + +List> zeroMatrix(int rows, int cols) => + List.generate(rows, (_) => List.filled(cols, 0.0)); + +List _mergeStops(List stops) { + List merged = []; + if (stops.length < 2) return stops; + + // Should be applied after places have been found + List toMerge = []; + + void merge() { + if (toMerge.isEmpty) return; + GeoLocation geoLocation = _computeCentroid(toMerge); + DateTime arr = toMerge.first.arrival; + DateTime dep = toMerge.last.departure; + Stop s = Stop(geoLocation, arr, dep, toMerge.first.placeId); + merged.add(s); + toMerge = []; + } + + for (Stop stop in stops) { + // If stop is noisy, just add it to the merged list, don't do anything to it + if (stop.placeId == -1) { + merged.add(stop); + } else { + // If no stops to merge, we cannot merge and we therefore add the current + // stop and go to the next one + if (toMerge.isEmpty) { + toMerge.add(stop); + } + + // Otherwise check if we should add it or merge + else { + if (stop.placeId != toMerge.last.placeId) { + merge(); + } + toMerge.add(stop); + } + } + } + + // Merge remaining stops in the toMerge list + merge(); + + return merged; +} diff --git a/packages/mobility_features/mobility_features_example/ios/Podfile b/packages/mobility_features/mobility_features_example/ios/Podfile deleted file mode 100644 index 1e8c3c90a..000000000 --- a/packages/mobility_features/mobility_features_example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '9.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/mobility_features/pubspec.yaml b/packages/mobility_features/pubspec.yaml index 51098a6bf..0d254b9d4 100644 --- a/packages/mobility_features/pubspec.yaml +++ b/packages/mobility_features/pubspec.yaml @@ -1,10 +1,11 @@ name: mobility_features description: Calculation of real-time mobility features like places, stops, and home stay -version: 4.0.1 +version: 6.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/ environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" dependencies: flutter: @@ -12,9 +13,14 @@ dependencies: simple_cluster: ^0.3.0 stats: ^2.0.0 path_provider: ^2.0.2 + json_annotation: ^4.8.0 + carp_serializable: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: any + json_serializable: any + build_runner: any flutter: diff --git a/packages/mobility_features/test/mobility_features_test.dart b/packages/mobility_features/test/mobility_features_test.dart index df215cbf3..9cf1b8d87 100644 --- a/packages/mobility_features/test/mobility_features_test.dart +++ b/packages/mobility_features/test/mobility_features_test.dart @@ -3,13 +3,13 @@ library mobility_test; import 'dart:async'; import 'package:mobility_features/mobility_features.dart'; import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:carp_serializable/carp_serializable.dart'; part 'test_utils.dart'; void main() async { - JsonEncoder jsonEncoder = JsonEncoder.withIndent('\t'); DateTime jan01 = DateTime(2020, 01, 01); // Poppelgade 7, home @@ -64,11 +64,11 @@ void main() async { /// Todays data List locationSamples = [ // 5 hours spent at home - LocationSample(pos1, date.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 6, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 6, minutes: 0))), - LocationSample(pos2, date.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, date.add(Duration(hours: 9, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, date.add(const Duration(hours: 9, minutes: 30))), ]; /// Save @@ -87,15 +87,15 @@ void main() async { List samples = [ // 5 hours spent at home - LocationSample(pos1, jan01.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 9, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 9, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 21, minutes: 0))), - LocationSample( - pos1, jan01.add(Duration(hours: 23, minutes: 59, seconds: 59))), + LocationSample(pos1, jan01.add(const Duration(hours: 21, minutes: 0))), + LocationSample(pos1, + jan01.add(const Duration(hours: 23, minutes: 59, seconds: 59))), ]; await MobilityFeatures().saveSamples(samples); @@ -109,38 +109,38 @@ void main() async { List samplesNoDuplicates = [ // 5 hours spent at home - LocationSample(pos1, jan01.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 9, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 9, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 21, minutes: 0))), - LocationSample( - pos1, jan01.add(Duration(hours: 23, minutes: 59, seconds: 59))), + LocationSample(pos1, jan01.add(const Duration(hours: 21, minutes: 0))), + LocationSample(pos1, + jan01.add(const Duration(hours: 23, minutes: 59, seconds: 59))), ]; /// Todays data List samplesWithDuplicates = [ // 5 hours spent at home - LocationSample(pos1, jan01.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 9, minutes: 0))), - - LocationSample(pos1, jan01.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), - - LocationSample(pos1, jan01.add(Duration(hours: 21, minutes: 0))), - LocationSample( - pos1, jan01.add(Duration(hours: 23, minutes: 59, seconds: 59))), + LocationSample(pos1, jan01.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 9, minutes: 0))), + + LocationSample(pos1, jan01.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), + + LocationSample(pos1, jan01.add(const Duration(hours: 21, minutes: 0))), + LocationSample(pos1, + jan01.add(const Duration(hours: 23, minutes: 59, seconds: 59))), ]; final samples = MobilityFeatures.uniqueElements(samplesWithDuplicates); @@ -152,17 +152,17 @@ void main() async { List samples = [ /// Location 1 - LocationSample(pos1, jan01.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, jan01.add(Duration(hours: 6, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, jan01.add(const Duration(hours: 6, minutes: 0))), /// Location 2 - LocationSample(pos2, jan01.add(Duration(hours: 8, minutes: 0))), - LocationSample(pos2, jan01.add(Duration(hours: 9, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 8, minutes: 0))), + LocationSample(pos2, jan01.add(const Duration(hours: 9, minutes: 0))), /// Location 1 - LocationSample(pos1, jan01.add(Duration(hours: 21, minutes: 0))), - LocationSample( - pos1, jan01.add(Duration(hours: 23, minutes: 59, seconds: 59))), + LocationSample(pos1, jan01.add(const Duration(hours: 21, minutes: 0))), + LocationSample(pos1, + jan01.add(const Duration(hours: 23, minutes: 59, seconds: 59))), ]; // Save samples, one by one @@ -181,30 +181,31 @@ void main() async { List samples = [ /// Location 1 (Home) - LocationSample(pos1, date.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 1, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 2, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 8, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 1, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 2, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 8, minutes: 0))), /// end of stop 1 /// Location 2 - LocationSample(pos2, date.add(Duration(hours: 8, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 9, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 12, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 8, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 9, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 12, minutes: 30))), /// end of stop 2 /// Gap in data // Location 1 (Home) - LocationSample(pos1, date.add(Duration(hours: 16, seconds: 1))), - LocationSample(pos1, date.add(Duration(hours: 20, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 16, seconds: 1))), + LocationSample(pos1, date.add(const Duration(hours: 20, minutes: 0))), /// end of stop 3 // Location 0 (Home), New day - LocationSample(pos1, date.add(Duration(days: 1, hours: 0, minutes: 2))), + LocationSample( + pos1, date.add(const Duration(days: 1, hours: 0, minutes: 2))), ]; // Create stream controller to stream the individual samples @@ -220,8 +221,8 @@ void main() async { // Listen to the Context stream Stream contextStream = MobilityFeatures().contextStream; contextStream.listen(expectAsync1((c) { - printList(c.stops); - print(c.toJson()); + printList(c.stops!); + print(toJsonString(c.toJson())); }, count: expectedContexts)); // Stream all the samples one by one @@ -234,7 +235,7 @@ void main() async { test('Stream LocationSamples with path between locations', () async { void onContext(MobilityContext mc) { print(mc.toJson()); - printList(mc.stops); + printList(mc.stops!); } flushFiles(); @@ -242,57 +243,58 @@ void main() async { List samples = [ /// Location 1 (Home) - LocationSample(pos1, date.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 1, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 2, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 8, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 1, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 2, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 8, minutes: 0))), /// end of stop 1 /// Path to Location 1 LocationSample(GeoLocation(55.691806, 12.557528), - date.add(Duration(hours: 8, minutes: 1))), + date.add(const Duration(hours: 8, minutes: 1))), LocationSample(GeoLocation(55.691419, 12.556970), - date.add(Duration(hours: 8, minutes: 2))), + date.add(const Duration(hours: 8, minutes: 2))), LocationSample(GeoLocation(55.691081, 12.556455), - date.add(Duration(hours: 8, minutes: 3))), + date.add(const Duration(hours: 8, minutes: 3))), LocationSample(GeoLocation(55.690706, 12.555875), - date.add(Duration(hours: 8, minutes: 4))), + date.add(const Duration(hours: 8, minutes: 4))), LocationSample(GeoLocation(55.690434, 12.555457), - date.add(Duration(hours: 8, minutes: 5))), + date.add(const Duration(hours: 8, minutes: 5))), LocationSample(GeoLocation(55.690161, 12.555060), - date.add(Duration(hours: 8, minutes: 6))), + date.add(const Duration(hours: 8, minutes: 6))), LocationSample(GeoLocation(55.690542, 12.554481), - date.add(Duration(hours: 8, minutes: 7))), + date.add(const Duration(hours: 8, minutes: 7))), LocationSample(GeoLocation(55.690801, 12.550164), - date.add(Duration(hours: 8, minutes: 8))), + date.add(const Duration(hours: 8, minutes: 8))), LocationSample(GeoLocation(55.690825, 12.544910), - date.add(Duration(hours: 8, minutes: 9))), + date.add(const Duration(hours: 8, minutes: 9))), LocationSample(GeoLocation(55.689685, 12.543661), - date.add(Duration(hours: 8, minutes: 15))), + date.add(const Duration(hours: 8, minutes: 15))), LocationSample(GeoLocation(55.688083, 12.541852), - date.add(Duration(hours: 8, minutes: 20))), + date.add(const Duration(hours: 8, minutes: 20))), LocationSample(GeoLocation(55.686007, 12.539484), - date.add(Duration(hours: 8, minutes: 29))), + date.add(const Duration(hours: 8, minutes: 29))), /// Location 1 - LocationSample(pos2, date.add(Duration(hours: 8, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 10, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 11, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 12, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 8, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 10, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 11, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 12, minutes: 30))), /// end of stop 2 /// Gap in data from 12:30 to 16:00 /// Location 0 (Home) - LocationSample(pos1, date.add(Duration(hours: 16, seconds: 1))), - LocationSample(pos1, date.add(Duration(hours: 20, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 16, seconds: 1))), + LocationSample(pos1, date.add(const Duration(hours: 20, minutes: 0))), /// end of stop 3 /// Location 0 (Home), New day - LocationSample(pos1, date.add(Duration(days: 1, hours: 0, minutes: 2))), + LocationSample( + pos1, date.add(const Duration(days: 1, hours: 0, minutes: 2))), ]; /// Create stream controller to stream the individual samples @@ -322,44 +324,45 @@ void main() async { List samples = [ /// Location 1 (Home) - LocationSample(pos1, date.add(Duration(hours: 0, minutes: 0))), - LocationSample(pos1, date.add(Duration(hours: 1, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 0, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 1, minutes: 0))), /// end of stop 1 - LocationSample(pos1, date.add(Duration(hours: 2, minutes: 0))) + LocationSample(pos1, date.add(const Duration(hours: 2, minutes: 0))) .addNoise(), - LocationSample(pos1, date.add(Duration(hours: 8, minutes: 0))) + LocationSample(pos1, date.add(const Duration(hours: 8, minutes: 0))) .addNoise(), /// end of stop 2 /// Location 1 - LocationSample(pos2, date.add(Duration(hours: 8, minutes: 30))), - LocationSample(pos2, date.add(Duration(hours: 9, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 8, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 9, minutes: 30))), /// end of stop 3 - LocationSample(pos2, date.add(Duration(hours: 10, minutes: 30))) + LocationSample(pos2, date.add(const Duration(hours: 10, minutes: 30))) .addNoise(), - LocationSample(pos2, date.add(Duration(hours: 11, minutes: 30))) + LocationSample(pos2, date.add(const Duration(hours: 11, minutes: 30))) .addNoise(), /// end of stop 4 - LocationSample(pos2, date.add(Duration(hours: 12, minutes: 00))), - LocationSample(pos2, date.add(Duration(hours: 12, minutes: 15))), - LocationSample(pos2, date.add(Duration(hours: 12, minutes: 30))), + LocationSample(pos2, date.add(const Duration(hours: 12, minutes: 00))), + LocationSample(pos2, date.add(const Duration(hours: 12, minutes: 15))), + LocationSample(pos2, date.add(const Duration(hours: 12, minutes: 30))), /// end of stop 5 /// Gap in data (should get interpolated to Location 1) /// Location 0 (Home) - LocationSample(pos1, date.add(Duration(hours: 16, seconds: 1))), - LocationSample(pos1, date.add(Duration(hours: 20, minutes: 0))), + LocationSample(pos1, date.add(const Duration(hours: 16, seconds: 1))), + LocationSample(pos1, date.add(const Duration(hours: 20, minutes: 0))), /// end of stop 6 /// Location 0 (Home), New day - LocationSample(pos1, date.add(Duration(days: 1, hours: 0, minutes: 2))), + LocationSample( + pos1, date.add(const Duration(days: 1, hours: 0, minutes: 2))), ]; /// Create stream controller to stream the individual samples @@ -375,7 +378,7 @@ void main() async { /// Listen to the Context stream Stream contextStream = MobilityFeatures().contextStream; contextStream.listen( - expectAsync1((c) => printList(c.stops), count: expectedContexts)); + expectAsync1((c) => printList(c.stops!), count: expectedContexts)); // Stream all the samples one by one for (LocationSample s in samples) { @@ -390,11 +393,11 @@ void main() async { final samples = loadDataSet(); print(samples.length); - final dates = samples.map((e) => e.datetime.midnight).toSet().toList(); + final dates = samples.map((e) => e.dateTime.midnight).toSet().toList(); printList(dates); final onLastDate = - samples.where((e) => e.datetime.midnight == dates[1]).toList(); + samples.where((e) => e.dateTime.midnight == dates[1]).toList(); print(onLastDate.length); print(onLastDate.last); @@ -407,22 +410,22 @@ void main() async { mobilityStream.listen(expectAsync1((event) { print('=' * 150); - print( - "Mobility Context Received: ${jsonEncoder.convert(event.toJson())}"); + print("Mobility Context Received: ${toJsonString(event.toJson())}"); print('-' * 50); print("STOPS"); - printList(event.stops); + printList(event.stops!); print("ALL PLACES"); - printList(event.places); + printList(event.places!); print("SIGNIFICANT PLACES"); - printList(event.significantPlaces); + printList(event.significantPlaces!); print( - "TOTAL DURATION (STOPS): ${event.stops.map((p) => p.duration).reduce((a, b) => a + b)}"); + "TOTAL DURATION (STOPS): ${event.stops?.map((p) => p.duration).reduce((a, b) => a + b)}"); }, count: 283)); - onLastDate.forEach((e) { + for (var e in onLastDate) { controller.add(e); - }); + } + controller.close(); }); }); diff --git a/packages/mobility_features/test/test_utils.dart b/packages/mobility_features/test/test_utils.dart index 3aa0cf25f..a7606115a 100644 --- a/packages/mobility_features/test/test_utils.dart +++ b/packages/mobility_features/test/test_utils.dart @@ -1,4 +1,4 @@ -part of mobility_test; +part of 'mobility_features_test.dart'; const String datasetPath = 'lib/data/example-multi.json'; const String testDataDir = 'test/testdata'; @@ -10,16 +10,16 @@ Duration takeTime(DateTime start, DateTime end) { /// Clean file every time test is run void flushFiles() async { - File samples = new File('$testDataDir/location_samples.json'); - File stops = new File('$testDataDir/stops.json'); - File moves = new File('$testDataDir/moves.json'); + File samples = File('$testDataDir/location_samples.json'); + File stops = File('$testDataDir/stops.json'); + File moves = File('$testDataDir/moves.json'); await samples.writeAsString(''); await stops.writeAsString(''); await moves.writeAsString(''); } -void printList(List l) { +void printList(List l) { for (int i = 0; i < l.length; i++) { print('[$i] ${l[i]}'); } @@ -36,20 +36,22 @@ class LocationDTO { } List loadDataSet() { - File f = new File('$testDataDir/data-example-munich.json'); + File f = File('$testDataDir/data-example-munich.json'); String content = f.readAsStringSync(); List lines = content.split('\n'); List samples = []; - lines.forEach((e) { + for (var e in lines) { try { - Map m = json.decode(e); - GeoLocation geoLocation = - GeoLocation(double.parse(m['lat']), double.parse(m['lon'])); - DateTime dateTime = - DateTime.fromMillisecondsSinceEpoch(int.parse(m['datetime'])); + Map m = json.decode(e) as Map; + GeoLocation geoLocation = GeoLocation( + double.parse(m['lat'] as String), double.parse(m['lon'] as String)); + DateTime dateTime = DateTime.fromMillisecondsSinceEpoch( + int.parse(m['datetime'] as String)); samples.add(LocationSample(geoLocation, dateTime)); - } catch (error) {} - }); + } catch (error) { + print('ERROR - $error'); + } + } return samples; } diff --git a/packages/mobility_features/test/testdata/location_samples.json b/packages/mobility_features/test/testdata/location_samples.json index 3f290383b..e69de29bb 100644 --- a/packages/mobility_features/test/testdata/location_samples.json +++ b/packages/mobility_features/test/testdata/location_samples.json @@ -1,90 +0,0 @@ -{"geo_location":{"latitude":48.171281653157905,"longitude":11.563218639973703},"datetime":"1581633183541"} -{"geo_location":{"latitude":48.1713003791387,"longitude":11.563167221184242},"datetime":"1581633189548"} -{"geo_location":{"latitude":48.17127219694427,"longitude":11.563203045308759},"datetime":"1581633195552"} -{"geo_location":{"latitude":48.17126526291558,"longitude":11.56321206673613},"datetime":"1581633198223"} -{"geo_location":{"latitude":48.17126389624605,"longitude":11.563214525559026},"datetime":"1581633198223"} -{"geo_location":{"latitude":48.171286671508035,"longitude":11.563189301036731},"datetime":"1581633201547"} -{"geo_location":{"latitude":48.17128164382298,"longitude":11.563204642645978},"datetime":"1581633207537"} -{"geo_location":{"latitude":48.17129498783873,"longitude":11.563212135816345},"datetime":"1581633213544"} -{"geo_location":{"latitude":48.17128662628747,"longitude":11.563238443120047},"datetime":"1581633219550"} -{"geo_location":{"latitude":48.171306394859734,"longitude":11.563227110801288},"datetime":"1581633225545"} -{"geo_location":{"latitude":48.17129425356578,"longitude":11.563191806114972},"datetime":"1581633230140"} -{"geo_location":{"latitude":48.171293689458764,"longitude":11.563196412330011},"datetime":"1581633286547"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633288141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633294141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633300141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633301141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633302144"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633305142"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633311141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633318141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633324143"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633330142"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633335144"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633341141"} -{"geo_location":{"latitude":48.17128943954254,"longitude":11.563195460232395},"datetime":"1581633348141"} -{"geo_location":{"latitude":48.17128127572143,"longitude":11.56317741341416},"datetime":"1581633364555"} -{"geo_location":{"latitude":48.17130290793368,"longitude":11.563169528635354},"datetime":"1581633370544"} -{"geo_location":{"latitude":48.17128704926877,"longitude":11.563200777109483},"datetime":"1581633376549"} -{"geo_location":{"latitude":48.17129273842976,"longitude":11.56321956053865},"datetime":"1581633382553"} -{"geo_location":{"latitude":48.17129449851463,"longitude":11.563224853290832},"datetime":"1581633383188"} -{"geo_location":{"latitude":48.17129497397242,"longitude":11.563226234435081},"datetime":"1581633383188"} -{"geo_location":{"latitude":48.17128941653813,"longitude":11.563191167393118},"datetime":"1581633388141"} -{"geo_location":{"latitude":48.17128941653813,"longitude":11.563191167393118},"datetime":"1581633389142"} -{"geo_location":{"latitude":48.17128941653813,"longitude":11.563191167393118},"datetime":"1581633394141"} -{"geo_location":{"latitude":48.171290987350105,"longitude":11.563187340478425},"datetime":"1581633407559"} -{"geo_location":{"latitude":48.17128583500631,"longitude":11.56315969457978},"datetime":"1581633413554"} -{"geo_location":{"latitude":48.171286973233535,"longitude":11.56316378379428},"datetime":"1581633419548"} -{"geo_location":{"latitude":48.17128676487958,"longitude":11.56316469101152},"datetime":"1581633424195"} -{"geo_location":{"latitude":48.171286663382034,"longitude":11.563165235349599},"datetime":"1581633424195"} -{"geo_location":{"latitude":48.17129130944989,"longitude":11.563178715745625},"datetime":"1581633425560"} -{"geo_location":{"latitude":48.17130733749713,"longitude":11.563212775898416},"datetime":"1581633431552"} -{"geo_location":{"latitude":48.17130542586099,"longitude":11.563196774852665},"datetime":"1581633437560"} -{"geo_location":{"latitude":48.17129326570097,"longitude":11.563160108856371},"datetime":"1581633443550"} -{"geo_location":{"latitude":48.171291635570256,"longitude":11.563188545890023},"datetime":"1581633449635"} -{"geo_location":{"latitude":48.17129870532947,"longitude":11.56321129357901},"datetime":"1581633455545"} -{"geo_location":{"latitude":48.171314378413,"longitude":11.563190104015941},"datetime":"1581633461550"} -{"geo_location":{"latitude":48.171312552986805,"longitude":11.563188678575338},"datetime":"1581633467555"} -{"geo_location":{"latitude":48.17129109679785,"longitude":11.563196082530137},"datetime":"1581633473552"} -{"geo_location":{"latitude":48.17132280064145,"longitude":11.563191398671746},"datetime":"1581633479552"} -{"geo_location":{"latitude":48.1712941704202,"longitude":11.56321293837407},"datetime":"1581633485549"} -{"geo_location":{"latitude":48.171299601547645,"longitude":11.563196226678375},"datetime":"1581633491551"} -{"geo_location":{"latitude":48.17131438615358,"longitude":11.563168362464546},"datetime":"1581633497551"} -{"geo_location":{"latitude":48.17128123960417,"longitude":11.563189026178073},"datetime":"1581633503553"} -{"geo_location":{"latitude":48.17132011829338,"longitude":11.563226292518822},"datetime":"1581633509538"} -{"geo_location":{"latitude":48.17130252395954,"longitude":11.563211057067306},"datetime":"1581633515540"} -{"geo_location":{"latitude":48.17129798979105,"longitude":11.563191975940216},"datetime":"1581633521548"} -{"geo_location":{"latitude":48.17131656003656,"longitude":11.563210741303022},"datetime":"1581633527544"} -{"geo_location":{"latitude":48.17131863514652,"longitude":11.56319862504117},"datetime":"1581633533545"} -{"geo_location":{"latitude":48.1713070561953,"longitude":11.563198433655048},"datetime":"1581633539543"} -{"geo_location":{"latitude":48.17130236927287,"longitude":11.563201300216122},"datetime":"1581633545531"} -{"geo_location":{"latitude":48.17129521056969,"longitude":11.56319943699132},"datetime":"1581633550540"} -{"geo_location":{"latitude":48.17131673866101,"longitude":11.563201412586812},"datetime":"1581633556539"} -{"geo_location":{"latitude":48.17130397034964,"longitude":11.563203172068816},"datetime":"1581633562551"} -{"geo_location":{"latitude":48.171288536883395,"longitude":11.563204886747661},"datetime":"1581633568553"} -{"geo_location":{"latitude":48.17130760267435,"longitude":11.563196954817178},"datetime":"1581633574547"} -{"geo_location":{"latitude":48.171280680863305,"longitude":11.563215972877181},"datetime":"1581633580542"} -{"geo_location":{"latitude":48.171296080313326,"longitude":11.56322328691332},"datetime":"1581633586541"} -{"geo_location":{"latitude":48.17132510439827,"longitude":11.563184492969626},"datetime":"1581633592554"} -{"geo_location":{"latitude":48.171309745958155,"longitude":11.563212691472879},"datetime":"1581633598543"} -{"geo_location":{"latitude":48.17130980868023,"longitude":11.563223221990256},"datetime":"1581633604549"} -{"geo_location":{"latitude":48.17130116552479,"longitude":11.563204770731598},"datetime":"1581633610547"} -{"geo_location":{"latitude":48.17132024048344,"longitude":11.563197098288784},"datetime":"1581633616553"} -{"geo_location":{"latitude":48.171299319142854,"longitude":11.563184923043417},"datetime":"1581633622547"} -{"geo_location":{"latitude":48.171310790416754,"longitude":11.563198310967506},"datetime":"1581633628544"} -{"geo_location":{"latitude":48.17131589082105,"longitude":11.563213775430425},"datetime":"1581633634615"} -{"geo_location":{"latitude":48.17131124461477,"longitude":11.5632183996362},"datetime":"1581633640550"} -{"geo_location":{"latitude":48.17132126286372,"longitude":11.563221820856478},"datetime":"1581633646551"} -{"geo_location":{"latitude":48.17130773221799,"longitude":11.56319857289543},"datetime":"1581633652555"} -{"geo_location":{"latitude":48.17133161288919,"longitude":11.563206664336509},"datetime":"1581633658545"} -{"geo_location":{"latitude":48.17132725995403,"longitude":11.563204133995162},"datetime":"1581633664545"} -{"geo_location":{"latitude":48.17131174010998,"longitude":11.563214543661774},"datetime":"1581633670547"} -{"geo_location":{"latitude":48.17131006002293,"longitude":11.563231760120583},"datetime":"1581633676549"} -{"geo_location":{"latitude":48.17131857591096,"longitude":11.563203957262328},"datetime":"1581633682549"} -{"geo_location":{"latitude":48.1713001793906,"longitude":11.563207264402903},"datetime":"1581633688547"} -{"geo_location":{"latitude":48.171320995827394,"longitude":11.56320571687284},"datetime":"1581633694551"} -{"geo_location":{"latitude":48.171289834325194,"longitude":11.563235278192037},"datetime":"1581633700537"} -{"geo_location":{"latitude":48.171305118409826,"longitude":11.563221094539754},"datetime":"1581633706547"} -{"geo_location":{"latitude":48.171306756445375,"longitude":11.563214587831721},"datetime":"1581633712549"} -{"geo_location":{"latitude":48.17129139009002,"longitude":11.563199191004612},"datetime":"1581633718547"} -{"geo_location":{"latitude":48.17131649786042,"longitude":11.56319249730382},"datetime":"1581633724548"} diff --git a/packages/mobility_features/test/testdata/moves.json b/packages/mobility_features/test/testdata/moves.json index 0f2ba4cc3..679fd2845 100644 --- a/packages/mobility_features/test/testdata/moves.json +++ b/packages/mobility_features/test/testdata/moves.json @@ -1,25 +1,25 @@ -{"stop_from":{"geo_location":{"latitude":48.171386612651546,"longitude":11.563414930789028},"place_id":0,"arrival":1581548434570,"departure":1581573471000},"stop_to":{"geo_location":{"latitude":48.15180786902203,"longitude":11.570675076139496},"place_id":1,"arrival":1581574208000,"departure":1581574286000},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.15180786902203,"longitude":11.570675076139496},"place_id":1,"arrival":1581574208000,"departure":1581574286000},"stop_to":{"geo_location":{"latitude":48.1415814591214,"longitude":11.568183708822179},"place_id":2,"arrival":1581574887999,"departure":1581579064202},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.1415814591214,"longitude":11.568183708822179},"place_id":2,"arrival":1581574887999,"departure":1581579064202},"stop_to":{"geo_location":{"latitude":48.1504296953681,"longitude":11.566743217383225},"place_id":3,"arrival":1581579777999,"departure":1581579798999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.1504296953681,"longitude":11.566743217383225},"place_id":3,"arrival":1581579777999,"departure":1581579798999},"stop_to":{"geo_location":{"latitude":48.16787021104484,"longitude":11.565147249278777},"place_id":4,"arrival":1581580295999,"departure":1581580356999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.16787021104484,"longitude":11.565147249278777},"place_id":4,"arrival":1581580295999,"departure":1581580356999},"stop_to":{"geo_location":{"latitude":48.17140848487236,"longitude":11.563477734901618},"place_id":0,"arrival":1581580476999,"departure":1581581492999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.17140848487236,"longitude":11.563477734901618},"place_id":0,"arrival":1581580476999,"departure":1581581492999},"stop_to":{"geo_location":{"latitude":48.166493289434975,"longitude":11.590099831774715},"place_id":5,"arrival":1581581964999,"departure":1581581986999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.166493289434975,"longitude":11.590099831774715},"place_id":5,"arrival":1581581964999,"departure":1581581986999},"stop_to":{"geo_location":{"latitude":48.21135811752502,"longitude":11.61641499268229},"place_id":6,"arrival":1581582720999,"departure":1581582748999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.21135811752502,"longitude":11.61641499268229},"place_id":6,"arrival":1581582720999,"departure":1581582748999},"stop_to":{"geo_location":{"latitude":48.26505202402048,"longitude":11.651737746005226},"place_id":7,"arrival":1581583114155,"departure":1581583165198},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26505202402048,"longitude":11.651737746005226},"place_id":7,"arrival":1581583114155,"departure":1581583165198},"stop_to":{"geo_location":{"latitude":48.26254938066689,"longitude":11.668640250356418},"place_id":8,"arrival":1581583481000,"departure":1581583508999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26254938066689,"longitude":11.668640250356418},"place_id":8,"arrival":1581583481000,"departure":1581583508999},"stop_to":{"geo_location":{"latitude":48.26257763760641,"longitude":11.666777112529939},"place_id":9,"arrival":1581583614000,"departure":1581583903404},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26257763760641,"longitude":11.666777112529939},"place_id":9,"arrival":1581583614000,"departure":1581583903404},"stop_to":{"geo_location":{"latitude":48.262125438156204,"longitude":11.668677326491046},"place_id":8,"arrival":1581585049309,"departure":1581585159252},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.262125438156204,"longitude":11.668677326491046},"place_id":8,"arrival":1581585049309,"departure":1581585159252},"stop_to":{"geo_location":{"latitude":48.26277057065003,"longitude":11.666868257639507},"place_id":9,"arrival":1581585262142,"departure":1581587784367},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26277057065003,"longitude":11.666868257639507},"place_id":9,"arrival":1581585262142,"departure":1581587784367},"stop_to":{"geo_location":{"latitude":48.268050906046824,"longitude":11.671886509382954},"place_id":10,"arrival":1581588985438,"departure":1581590752024},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.268050906046824,"longitude":11.671886509382954},"place_id":10,"arrival":1581588985438,"departure":1581590752024},"stop_to":{"geo_location":{"latitude":48.26233590463306,"longitude":11.668574732199161},"place_id":8,"arrival":1581591495000,"departure":1581593240999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26233590463306,"longitude":11.668574732199161},"place_id":8,"arrival":1581591495000,"departure":1581593240999},"stop_to":{"geo_location":{"latitude":48.26286749915574,"longitude":11.666522133371},"place_id":9,"arrival":1581593402134,"departure":1581596197316},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26286749915574,"longitude":11.666522133371},"place_id":9,"arrival":1581593402134,"departure":1581596197316},"stop_to":{"geo_location":{"latitude":48.262504482026245,"longitude":11.668389434158957},"place_id":8,"arrival":1581596392804,"departure":1581596448469},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.262504482026245,"longitude":11.668389434158957},"place_id":8,"arrival":1581596392804,"departure":1581596448469},"stop_to":{"geo_location":{"latitude":48.26286828553107,"longitude":11.666566430605975},"place_id":9,"arrival":1581596662684,"departure":1581599426719},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26286828553107,"longitude":11.666566430605975},"place_id":9,"arrival":1581596662684,"departure":1581599426719},"stop_to":{"geo_location":{"latitude":48.26258553968282,"longitude":11.667927814298238},"place_id":8,"arrival":1581599829447,"departure":1581600214446},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.26258553968282,"longitude":11.667927814298238},"place_id":8,"arrival":1581599829447,"departure":1581600214446},"stop_to":{"geo_location":{"latitude":48.262844228101784,"longitude":11.666359408625585},"place_id":9,"arrival":1581600840883,"departure":1581606295366},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.262844228101784,"longitude":11.666359408625585},"place_id":9,"arrival":1581600840883,"departure":1581606295366},"stop_to":{"geo_location":{"latitude":48.262580403192686,"longitude":11.6678265961852},"place_id":8,"arrival":1581606502755,"departure":1581606803823},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.262580403192686,"longitude":11.6678265961852},"place_id":8,"arrival":1581606502755,"departure":1581606803823},"stop_to":{"geo_location":{"latitude":48.211442262912854,"longitude":11.616452106516075},"place_id":6,"arrival":1581607480000,"departure":1581607504000},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.211442262912854,"longitude":11.616452106516075},"place_id":6,"arrival":1581607480000,"departure":1581607504000},"stop_to":{"geo_location":{"latitude":48.19160641714296,"longitude":11.614063258104917},"place_id":11,"arrival":1581607722000,"departure":1581607755999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.19160641714296,"longitude":11.614063258104917},"place_id":11,"arrival":1581607722000,"departure":1581607755999},"stop_to":{"geo_location":{"latitude":48.16663433495479,"longitude":11.590159019361},"place_id":5,"arrival":1581608176000,"departure":1581608202000},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.16663433495479,"longitude":11.590159019361},"place_id":5,"arrival":1581608176000,"departure":1581608202000},"stop_to":{"geo_location":{"latitude":48.1668372949315,"longitude":11.5782162847473},"place_id":12,"arrival":1581608423999,"departure":1581608702999},"distance":0.0} -{"stop_from":{"geo_location":{"latitude":48.1668372949315,"longitude":11.5782162847473},"place_id":12,"arrival":1581608423999,"departure":1581608702999},"stop_to":{"geo_location":{"latitude":48.1713134906651,"longitude":11.56320208489327},"place_id":0,"arrival":1581609221143,"departure":1581633141553},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171386612651546,"longitude":11.563414930789028},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1713134906651,"longitude":11.56320208489327},"placeId":0,"arrival":"2020-02-13T16:53:41.143","departure":"2020-02-13T23:32:21.553"},"distance":0.0} diff --git a/packages/mobility_features/test/testdata/stops.json b/packages/mobility_features/test/testdata/stops.json index c8d9d23cd..8a59ba3ac 100644 --- a/packages/mobility_features/test/testdata/stops.json +++ b/packages/mobility_features/test/testdata/stops.json @@ -1,26 +1,26 @@ -{"geo_location":{"latitude":48.171386612651546,"longitude":11.563414930789028},"place_id":0,"arrival":1581548434570,"departure":1581573471000} -{"geo_location":{"latitude":48.15180786902203,"longitude":11.570675076139496},"place_id":1,"arrival":1581574208000,"departure":1581574286000} -{"geo_location":{"latitude":48.1415814591214,"longitude":11.568183708822179},"place_id":2,"arrival":1581574887999,"departure":1581579064202} -{"geo_location":{"latitude":48.1504296953681,"longitude":11.566743217383225},"place_id":3,"arrival":1581579777999,"departure":1581579798999} -{"geo_location":{"latitude":48.16787021104484,"longitude":11.565147249278777},"place_id":4,"arrival":1581580295999,"departure":1581580356999} -{"geo_location":{"latitude":48.17140848487236,"longitude":11.563477734901618},"place_id":0,"arrival":1581580476999,"departure":1581581492999} -{"geo_location":{"latitude":48.166493289434975,"longitude":11.590099831774715},"place_id":5,"arrival":1581581964999,"departure":1581581986999} -{"geo_location":{"latitude":48.21135811752502,"longitude":11.61641499268229},"place_id":6,"arrival":1581582720999,"departure":1581582748999} -{"geo_location":{"latitude":48.26505202402048,"longitude":11.651737746005226},"place_id":7,"arrival":1581583114155,"departure":1581583165198} -{"geo_location":{"latitude":48.26254938066689,"longitude":11.668640250356418},"place_id":8,"arrival":1581583481000,"departure":1581583508999} -{"geo_location":{"latitude":48.26257763760641,"longitude":11.666777112529939},"place_id":9,"arrival":1581583614000,"departure":1581583903404} -{"geo_location":{"latitude":48.262125438156204,"longitude":11.668677326491046},"place_id":8,"arrival":1581585049309,"departure":1581585159252} -{"geo_location":{"latitude":48.26277057065003,"longitude":11.666868257639507},"place_id":9,"arrival":1581585262142,"departure":1581587784367} -{"geo_location":{"latitude":48.268050906046824,"longitude":11.671886509382954},"place_id":10,"arrival":1581588985438,"departure":1581590752024} -{"geo_location":{"latitude":48.26233590463306,"longitude":11.668574732199161},"place_id":8,"arrival":1581591495000,"departure":1581593240999} -{"geo_location":{"latitude":48.26286749915574,"longitude":11.666522133371},"place_id":9,"arrival":1581593402134,"departure":1581596197316} -{"geo_location":{"latitude":48.262504482026245,"longitude":11.668389434158957},"place_id":8,"arrival":1581596392804,"departure":1581596448469} -{"geo_location":{"latitude":48.26286828553107,"longitude":11.666566430605975},"place_id":9,"arrival":1581596662684,"departure":1581599426719} -{"geo_location":{"latitude":48.26258553968282,"longitude":11.667927814298238},"place_id":8,"arrival":1581599829447,"departure":1581600214446} -{"geo_location":{"latitude":48.262844228101784,"longitude":11.666359408625585},"place_id":9,"arrival":1581600840883,"departure":1581606295366} -{"geo_location":{"latitude":48.262580403192686,"longitude":11.6678265961852},"place_id":8,"arrival":1581606502755,"departure":1581606803823} -{"geo_location":{"latitude":48.211442262912854,"longitude":11.616452106516075},"place_id":6,"arrival":1581607480000,"departure":1581607504000} -{"geo_location":{"latitude":48.19160641714296,"longitude":11.614063258104917},"place_id":11,"arrival":1581607722000,"departure":1581607755999} -{"geo_location":{"latitude":48.16663433495479,"longitude":11.590159019361},"place_id":5,"arrival":1581608176000,"departure":1581608202000} -{"geo_location":{"latitude":48.1668372949315,"longitude":11.5782162847473},"place_id":12,"arrival":1581608423999,"departure":1581608702999} -{"geo_location":{"latitude":48.1713134906651,"longitude":11.56320208489327},"place_id":0,"arrival":1581609221143,"departure":1581633141553} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171386612651546,"longitude":11.563414930789028},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1713134906651,"longitude":11.56320208489327},"placeId":0,"arrival":"2020-02-13T16:53:41.143","departure":"2020-02-13T23:32:21.553"} diff --git a/packages/notifications/example/.flutter-plugins-dependencies b/packages/notifications/example/.flutter-plugins-dependencies index bce6c1b5c..77ab4de4d 100644 --- a/packages/notifications/example/.flutter-plugins-dependencies +++ b/packages/notifications/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"notifications","path":"/Users/bardram/dev/flutter-plugins/packages/notifications/","dependencies":[]}],"android":[{"name":"notifications","path":"/Users/bardram/dev/flutter-plugins/packages/notifications/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"notifications","dependencies":[]}],"date_created":"2021-07-22 09:08:42.769208","version":"2.2.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"notifications","path":"/Users/arata/Developer/carp/flutter-plugins/packages/notifications/","native_build":true,"dependencies":[]}],"android":[{"name":"notifications","path":"/Users/arata/Developer/carp/flutter-plugins/packages/notifications/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"notifications","dependencies":[]}],"date_created":"2024-10-30 13:38:55.333244","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/notifications/example/ios/Flutter/flutter_export_environment.sh b/packages/notifications/example/ios/Flutter/flutter_export_environment.sh index 0d97f7bb4..886abaadb 100755 --- a/packages/notifications/example/ios/Flutter/flutter_export_environment.sh +++ b/packages/notifications/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/hoffmatteo/dev/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/hoffmatteo/Desktop/CACHET/flutter-plugins/packages/notifications/example" +export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.24.3/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/arata/Developer/carp/flutter-plugins/packages/notifications/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" diff --git a/packages/pedometer/CHANGELOG.md b/packages/pedometer/CHANGELOG.md index e80a3d4ac..529cec1f2 100644 --- a/packages/pedometer/CHANGELOG.md +++ b/packages/pedometer/CHANGELOG.md @@ -1,9 +1,9 @@ -## 4.0.1 +## 4.0.2 - Updates Kotlin plugin and AGP. - Upgrade of `compileSdkVersion` to 33. - Upgrade to Dart 3. -- Small updates to example app +- Small updates to example app (asking for permissions) ## 3.0.0 diff --git a/packages/pedometer/example/.flutter-plugins-dependencies b/packages/pedometer/example/.flutter-plugins-dependencies index 4970aca48..8c805c999 100644 --- a/packages/pedometer/example/.flutter-plugins-dependencies +++ b/packages/pedometer/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"pedometer","path":"/Users/bardram/dev/flutter-plugins/packages/pedometer/","native_build":true,"dependencies":[]}],"android":[{"name":"pedometer","path":"/Users/bardram/dev/flutter-plugins/packages/pedometer/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"pedometer","dependencies":[]}],"date_created":"2023-06-20 19:23:27.397729","version":"3.10.5"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"pedometer","path":"/Users/arata/Developer/carp/flutter-plugins/packages/pedometer/","native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"/Users/arata/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/","native_build":true,"dependencies":[]}],"android":[{"name":"pedometer","path":"/Users/arata/Developer/carp/flutter-plugins/packages/pedometer/","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"/Users/arata/.pub-cache/hosted/pub.dev/permission_handler_android-12.0.13/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[{"name":"permission_handler_windows","path":"/Users/arata/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[]}],"web":[{"name":"permission_handler_html","path":"/Users/arata/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+2/","dependencies":[]}]},"dependencyGraph":[{"name":"pedometer","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]}],"date_created":"2024-10-30 13:38:57.617363","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/pedometer/example/ios/Flutter/AppFrameworkInfo.plist b/packages/pedometer/example/ios/Flutter/AppFrameworkInfo.plist index a3e20815c..fa91fa9f5 100644 --- a/packages/pedometer/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/pedometer/example/ios/Flutter/AppFrameworkInfo.plist @@ -23,4 +23,4 @@ MinimumOSVersion 9.0 - \ No newline at end of file + diff --git a/packages/pedometer/example/ios/Flutter/Flutter.podspec b/packages/pedometer/example/ios/Flutter/Flutter.podspec index 2c4421cfe..98e163395 100644 --- a/packages/pedometer/example/ios/Flutter/Flutter.podspec +++ b/packages/pedometer/example/ios/Flutter/Flutter.podspec @@ -1,17 +1,17 @@ # -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# This is a generated file; do not edit or check into version control. +# This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. # Pod::Spec.new do |s| s.name = 'Flutter' s.version = '1.0.0' - s.summary = 'High-performance, high-fidelity mobile apps.' - s.homepage = 'https://flutter.io' - s.license = { :type => 'MIT' } + s.summary = 'A UI toolkit for beautiful and fast apps.' + s.homepage = 'https://flutter.dev' + s.license = { :type => 'BSD' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' + s.ios.deployment_target = '12.0' # Framework linking is handled by Flutter tooling, not CocoaPods. # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. s.vendored_frameworks = 'path/to/nothing' diff --git a/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh b/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh index be27e1299..4325b283e 100755 --- a/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh +++ b/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/bardram/dev/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/bardram/dev/flutter-plugins/packages/pedometer/example" +export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.24.3/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/arata/Developer/carp/flutter-plugins/packages/pedometer/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" diff --git a/packages/pedometer/example/ios/Podfile b/packages/pedometer/example/ios/Podfile index 252d9ec7c..2c068c404 100644 --- a/packages/pedometer/example/ios/Podfile +++ b/packages/pedometer/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/pedometer/example/ios/Runner.xcodeproj/project.pbxproj b/packages/pedometer/example/ios/Runner.xcodeproj/project.pbxproj index 79a9a9c9c..0dd899a26 100644 --- a/packages/pedometer/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/pedometer/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -163,7 +163,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -224,10 +224,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -238,6 +240,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -347,7 +350,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -363,14 +366,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = N6R2LB9629; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -434,7 +437,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -483,7 +486,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -501,14 +504,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = N6R2LB9629; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -533,14 +536,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = N6R2LB9629; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/packages/pedometer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pedometer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..e67b2808a 100644 --- a/packages/pedometer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/pedometer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/pedometer/example/lib/main.dart b/packages/pedometer/example/lib/main.dart index 1310d2c6e..2139f634b 100644 --- a/packages/pedometer/example/lib/main.dart +++ b/packages/pedometer/example/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:pedometer/pedometer.dart'; +import 'package:permission_handler/permission_handler.dart'; String formatDate(DateTime d) { return d.toString().substring(0, 19); @@ -56,10 +57,25 @@ class _MyAppState extends State { }); } - void initPlatformState() { + Future _checkActivityRecognitionPermission() async { + bool granted = await Permission.activityRecognition.isGranted; + + if (!granted) { + granted = await Permission.activityRecognition.request() == + PermissionStatus.granted; + } + + return granted; + } + + Future initPlatformState() async { + bool granted = await _checkActivityRecognitionPermission(); + if (!granted) { + // tell user, the app will not work + } + _pedestrianStatusStream = Pedometer.pedestrianStatusStream; - _pedestrianStatusStream - .listen(onPedestrianStatusChanged) + (await _pedestrianStatusStream.listen(onPedestrianStatusChanged)) .onError(onPedestrianStatusError); _stepCountStream = Pedometer.stepCountStream; diff --git a/packages/pedometer/example/pubspec.yaml b/packages/pedometer/example/pubspec.yaml index 725f653f4..06bd8a078 100644 --- a/packages/pedometer/example/pubspec.yaml +++ b/packages/pedometer/example/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: flutter: sdk: flutter + permission_handler: ^11.3.0 pedometer: # When depending on this package from a real application you should use: # pedometer: ^x.y.z @@ -17,7 +18,6 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ - cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: diff --git a/packages/pedometer/pubspec.yaml b/packages/pedometer/pubspec.yaml index f676ff350..cca1a29e0 100644 --- a/packages/pedometer/pubspec.yaml +++ b/packages/pedometer/pubspec.yaml @@ -1,6 +1,6 @@ name: pedometer description: A Pedometer and Step Detection package for Android and iOS. Step count is streamed as the platform updates it. -version: 4.0.1 +version: 4.0.2 homepage: https://github.com/cph-cachet/flutter-plugins environment: diff --git a/packages/screen_state/.fvmrc b/packages/screen_state/.fvmrc new file mode 100644 index 000000000..c300356c3 --- /dev/null +++ b/packages/screen_state/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/packages/screen_state/.gitignore b/packages/screen_state/.gitignore new file mode 100644 index 000000000..9e366fe3b --- /dev/null +++ b/packages/screen_state/.gitignore @@ -0,0 +1,3 @@ + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/packages/screen_state/CHANGELOG.md b/packages/screen_state/CHANGELOG.md index 2d0b19d97..4721e8f0e 100644 --- a/packages/screen_state/CHANGELOG.md +++ b/packages/screen_state/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.0.0 + +- Added support for iOS. +- Restructuring of the interaction between the plugin and native applications. + ## 3.0.1 - Reduced minSdk version to 23 diff --git a/packages/screen_state/README.md b/packages/screen_state/README.md index 2266df28b..03b02bf87 100644 --- a/packages/screen_state/README.md +++ b/packages/screen_state/README.md @@ -36,4 +36,11 @@ The stream can also be cancelled again by calling the `cancel()` method: void stopListening() { _subscription.cancel(); } -``` \ No newline at end of file +``` + +## Limitations + +#### iOS: +- This package will exclusively detect screen unlocks on iOS devices that are protected with a passcode. If the device lacks a passcode, it will solely detect the screen's on/off state; +- The detection doesn't function when the app is running in the iOS simulator. The plugin will only return a 'Screen_On' event. +- When the user sets their brightness to the lowest setting, it is possible that this generates a `SCREEN_ON` event, a limitation to the way that this plugin detects when the screen is off. diff --git a/packages/screen_state/example/.gitignore b/packages/screen_state/example/.gitignore new file mode 100644 index 000000000..29a3a5017 --- /dev/null +++ b/packages/screen_state/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/screen_state/example/.metadata b/packages/screen_state/example/.metadata index ade6bc987..b5c370f38 100644 --- a/packages/screen_state/example/.metadata +++ b/packages/screen_state/example/.metadata @@ -4,7 +4,42 @@ # This file should be version controlled and should not be manually edited. version: - revision: 8af6b2f038c1172e61d418869363a28dffec3cb4 - channel: stable + revision: "41456452f29d64e8deb623a3c927524bcf9f111b" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: android + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: ios + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: linux + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: macos + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: web + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + - platform: windows + create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/screen_state/example/analysis_options.yaml b/packages/screen_state/example/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/packages/screen_state/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/screen_state/example/android/.gitignore b/packages/screen_state/example/android/.gitignore new file mode 100644 index 000000000..6f568019d --- /dev/null +++ b/packages/screen_state/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/screen_state/example/android/app/build.gradle b/packages/screen_state/example/android/app/build.gradle index 40d90dec2..3578f3e4f 100644 --- a/packages/screen_state/example/android/app/build.gradle +++ b/packages/screen_state/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,26 +22,31 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 33 + namespace "com.example.example" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' } - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "dk.cachet.screen_state_example" + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion 23 - targetSdkVersion 33 + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -58,6 +64,4 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +dependencies {} diff --git a/packages/screen_state/example/android/app/src/debug/AndroidManifest.xml b/packages/screen_state/example/android/app/src/debug/AndroidManifest.xml index a03c35b31..399f6981d 100644 --- a/packages/screen_state/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/screen_state/example/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/packages/screen_state/example/android/app/src/main/AndroidManifest.xml b/packages/screen_state/example/android/app/src/main/AndroidManifest.xml index 84a90baf1..2710e5dc7 100644 --- a/packages/screen_state/example/android/app/src/main/AndroidManifest.xml +++ b/packages/screen_state/example/android/app/src/main/AndroidManifest.xml @@ -1,16 +1,10 @@ - - + - - diff --git a/packages/screen_state/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/screen_state/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 000000000..e793a000d --- /dev/null +++ b/packages/screen_state/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/screen_state/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/screen_state/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/screen_state/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/screen_state/example/android/app/src/main/res/values-night/styles.xml b/packages/screen_state/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/screen_state/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/screen_state/example/android/app/src/main/res/values/styles.xml b/packages/screen_state/example/android/app/src/main/res/values/styles.xml index 1f83a33fd..cb1ef8805 100644 --- a/packages/screen_state/example/android/app/src/main/res/values/styles.xml +++ b/packages/screen_state/example/android/app/src/main/res/values/styles.xml @@ -1,18 +1,18 @@ - - - diff --git a/packages/screen_state/example/android/app/src/profile/AndroidManifest.xml b/packages/screen_state/example/android/app/src/profile/AndroidManifest.xml index a03c35b31..399f6981d 100644 --- a/packages/screen_state/example/android/app/src/profile/AndroidManifest.xml +++ b/packages/screen_state/example/android/app/src/profile/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/packages/screen_state/example/android/build.gradle b/packages/screen_state/example/android/build.gradle index dd00b8ec8..e83fb5dac 100644 --- a/packages/screen_state/example/android/build.gradle +++ b/packages/screen_state/example/android/build.gradle @@ -1,12 +1,11 @@ buildscript { - ext.kotlin_version = '1.7.20' + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +13,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/screen_state/example/android/gradle.properties b/packages/screen_state/example/android/gradle.properties index 38c8d4544..598d13fee 100644 --- a/packages/screen_state/example/android/gradle.properties +++ b/packages/screen_state/example/android/gradle.properties @@ -1,4 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a4..000000000 Binary files a/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.properties index 774fae876..3c472b99c 100644 --- a/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/screen_state/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/screen_state/example/android/gradlew b/packages/screen_state/example/android/gradlew deleted file mode 100755 index 1b6c78733..000000000 --- a/packages/screen_state/example/android/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/packages/screen_state/example/android/gradlew.bat b/packages/screen_state/example/android/gradlew.bat deleted file mode 100644 index 107acd32c..000000000 --- a/packages/screen_state/example/android/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/packages/screen_state/example/android/settings.gradle b/packages/screen_state/example/android/settings.gradle index d3b6a4013..7cd712855 100644 --- a/packages/screen_state/example/android/settings.gradle +++ b/packages/screen_state/example/android/settings.gradle @@ -1,15 +1,29 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() -include ':app' + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + repositories { + google() + mavenCentral() + gradlePluginPortal() + } -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false +} + +include ":app" diff --git a/packages/screen_state/example/ios/.gitignore b/packages/screen_state/example/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/packages/screen_state/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/screen_state/example/ios/Flutter/AppFrameworkInfo.plist b/packages/screen_state/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a..7c5696400 100644 --- a/packages/screen_state/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/screen_state/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 12.0 diff --git a/packages/screen_state/example/ios/Flutter/Debug.xcconfig b/packages/screen_state/example/ios/Flutter/Debug.xcconfig index e8efba114..ec97fc6f3 100644 --- a/packages/screen_state/example/ios/Flutter/Debug.xcconfig +++ b/packages/screen_state/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/screen_state/example/ios/Flutter/Release.xcconfig b/packages/screen_state/example/ios/Flutter/Release.xcconfig index 399e9340e..c4855bfe2 100644 --- a/packages/screen_state/example/ios/Flutter/Release.xcconfig +++ b/packages/screen_state/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/screen_state/example/ios/Podfile b/packages/screen_state/example/ios/Podfile index 6697f0a53..d97f17e22 100644 --- a/packages/screen_state/example/ios/Podfile +++ b/packages/screen_state/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -10,78 +10,35 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - generated_key_values + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/packages/screen_state/example/ios/Runner.xcodeproj/project.pbxproj b/packages/screen_state/example/ios/Runner.xcodeproj/project.pbxproj index e02e926a0..0548f766b 100644 --- a/packages/screen_state/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/screen_state/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,18 +3,31 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 32C83F22EDB013A5A3756E58 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D046AA19DFF8C399B250FC /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8B3B6985881CA6E5CC4A08D3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4780D8A493E440DD9CF363D /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -31,9 +44,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 52BA1706B5253876613D90E4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 76D046AA19DFF8C399B250FC /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -42,19 +59,55 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CA3E07307946F57CBC6E8477 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + D2F5F5B0D77CEED366D0877D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + D40FEC992E5A86050ADA9172 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E4780D8A493E440DD9CF363D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F11AFF197ED0A0DD0101AA8D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F1E8B89737BE45CF520BDBE7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 07B45BB3578FB808A88D1625 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 32C83F22EDB013A5A3756E58 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8B3B6985881CA6E5CC4A08D3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 508FFBD0B3137313E52A90E3 /* Pods */ = { + isa = PBXGroup; + children = ( + D40FEC992E5A86050ADA9172 /* Pods-Runner.debug.xcconfig */, + 52BA1706B5253876613D90E4 /* Pods-Runner.release.xcconfig */, + F11AFF197ED0A0DD0101AA8D /* Pods-Runner.profile.xcconfig */, + CA3E07307946F57CBC6E8477 /* Pods-RunnerTests.debug.xcconfig */, + D2F5F5B0D77CEED366D0877D /* Pods-RunnerTests.release.xcconfig */, + F1E8B89737BE45CF520BDBE7 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +125,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 508FFBD0B3137313E52A90E3 /* Pods */, + FBF20B9D02BE7523F4E44B50 /* Frameworks */, ); sourceTree = ""; }; @@ -79,6 +135,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -90,7 +147,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -99,26 +155,49 @@ path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { + FBF20B9D02BE7523F4E44B50 /* Frameworks */ = { isa = PBXGroup; children = ( + E4780D8A493E440DD9CF363D /* Pods_Runner.framework */, + 76D046AA19DFF8C399B250FC /* Pods_RunnerTests.framework */, ); - name = "Supporting Files"; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + B55B8D965C6E6048224C47A5 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 07B45BB3578FB808A88D1625 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + F2C1791CFE3305550046F77E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7E088B4E60C7BF58505CCD92 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -135,9 +214,14 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -158,11 +242,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -179,10 +271,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -191,8 +285,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 7E088B4E60C7BF58505CCD92 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -205,9 +317,61 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B55B8D965C6E6048224C47A5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F2C1791CFE3305550046F77E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -219,6 +383,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -241,7 +413,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -281,7 +452,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -298,17 +469,14 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + INFOPLIST_KEY_CFBundleDisplayName = "Screen State Example"; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.screenStateExample; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -316,9 +484,58 @@ }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA3E07307946F57CBC6E8477 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2F5F5B0D77CEED366D0877D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F1E8B89737BE45CF520BDBE7 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -364,7 +581,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -374,7 +591,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -414,11 +630,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -432,17 +649,14 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + INFOPLIST_KEY_CFBundleDisplayName = "Screen State Example"; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.screenStateExample; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -459,17 +673,14 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + INFOPLIST_KEY_CFBundleDisplayName = "Screen State Example"; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dk.cachet.screenStateExample; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -480,6 +691,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/screen_state/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/screen_state/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/packages/screen_state/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/screen_state/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/screen_state/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/screen_state/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..8e3ca5dfe 100644 --- a/packages/screen_state/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/screen_state/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + - - + + diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf030..7353c41ec 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd967..797d452e4 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b0b..6ed2d933e 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118..4cd7b0099 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e7e..fe730945a 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc2306c..321773cd8 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd967..797d452e4 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8f5..502f463a9 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609..0ec303439 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609..0ec303439 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a..e9f5fea27 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39..84ac32ae7 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41e1..8953cba09 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f5853..0467bf12a 100644 Binary files a/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/packages/screen_state/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/screen_state/example/ios/Runner/Info.plist b/packages/screen_state/example/ios/Runner/Info.plist index b75d460fe..513a25d3f 100644 --- a/packages/screen_state/example/ios/Runner/Info.plist +++ b/packages/screen_state/example/ios/Runner/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Screen State Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - screen_state_example + example CFBundlePackageType APPL CFBundleShortVersionString @@ -39,7 +41,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/screen_state/example/ios/RunnerTests/RunnerTests.swift b/packages/screen_state/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/packages/screen_state/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/screen_state/example/pubspec.yaml b/packages/screen_state/example/pubspec.yaml index 4f32c97c4..4dde4fad2 100644 --- a/packages/screen_state/example/pubspec.yaml +++ b/packages/screen_state/example/pubspec.yaml @@ -1,70 +1,21 @@ name: screen_state_example description: Demonstrates how to use the screen_state plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev +version: 1.0.0+1 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.1.0 <4.0.0" dependencies: flutter: sdk: flutter - screen_state: - # When depending on this package from a real application you should use: - # screen_state: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.6 dev_dependencies: flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + uses-material-design: true \ No newline at end of file diff --git a/packages/screen_state/ios/Classes/SwiftScreenStatePlugin.swift b/packages/screen_state/ios/Classes/SwiftScreenStatePlugin.swift index 47ec31f4e..da5a57491 100644 --- a/packages/screen_state/ios/Classes/SwiftScreenStatePlugin.swift +++ b/packages/screen_state/ios/Classes/SwiftScreenStatePlugin.swift @@ -3,12 +3,89 @@ import UIKit public class SwiftScreenStatePlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "screen_state", binaryMessenger: registrar.messenger()) - let instance = SwiftScreenStatePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) + let screenStateDetector = ScreenStateDetector() + let channel = FlutterEventChannel.init(name: "screenStateEvents", binaryMessenger: registrar.messenger()) + channel.setStreamHandler(screenStateDetector) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { result("iOS " + UIDevice.current.systemVersion) } } + +// Convert from string to enum +enum ScreenState: String { + case on = "SCREEN_ON" + case off = "SCREEN_OFF" + case unlock = "SCREEN_UNLOCKED" + case unknown = "UNKNOWN" + + init(fromString string: String) { + switch string { + case "SCREEN_ON": + self = .on + case "SCREEN_OFF": + self = .off + case "SCREEN_UNLOCKED": + self = .unlock + default: + self = .unknown + } + } + +} + +public class ScreenStateDetector: NSObject, FlutterStreamHandler { + private var eventSink: FlutterEventSink? + private var lastState: ScreenState = .unknown + + private func handleEvent(screenState: ScreenState) { + if (screenState == .unknown || eventSink == nil) { + return + } + + eventSink!(screenState.rawValue) + } + + @objc + private func handleStateChange() { + if UIApplication.shared.isProtectedDataAvailable { + let screenState: ScreenState = UIScreen.main.brightness > 0 ? .on : .off + + if(screenState == .on && lastState == .unlock) { + return + } + + if screenState != lastState { + lastState = screenState + handleEvent(screenState: screenState) + } + } + } + + @objc + private func handleScreenLockChanged() { + if UIApplication.shared.isProtectedDataAvailable { + if (lastState == .on) { + lastState = .unlock + handleEvent(screenState: .unlock) + } + } + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + + NotificationCenter.default.addObserver(self, selector: #selector(handleStateChange), name: UIScreen.brightnessDidChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleScreenLockChanged), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleScreenLockChanged), name: UIApplication.protectedDataWillBecomeUnavailableNotification, object: nil) + + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + NotificationCenter.default.removeObserver(self) + eventSink = nil + return nil + } +} \ No newline at end of file diff --git a/packages/screen_state/ios/screen_state.podspec b/packages/screen_state/ios/screen_state.podspec index 52455a4df..fcf5486aa 100644 --- a/packages/screen_state/ios/screen_state.podspec +++ b/packages/screen_state/ios/screen_state.podspec @@ -4,14 +4,14 @@ # Pod::Spec.new do |s| s.name = 'screen_state' - s.version = '0.0.1' - s.summary = 'A new Flutter project.' + s.version = '1.0.0' + s.summary = 'Base plugin for screen state detection.' s.description = <<-DESC -A new Flutter project. +https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state/ios. DESC - s.homepage = 'http://example.com' + s.homepage = 'https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state/ios' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'Tokenlab' => 'luansilva@tokenlab.com.br' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' diff --git a/packages/screen_state/lib/screen_state.dart b/packages/screen_state/lib/screen_state.dart index 920caa70d..8fb2a4bc1 100644 --- a/packages/screen_state/lib/screen_state.dart +++ b/packages/screen_state/lib/screen_state.dart @@ -2,11 +2,51 @@ import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/services.dart'; -/// The type of screen state events coming from Android. -enum ScreenStateEvent { SCREEN_UNLOCKED, SCREEN_ON, SCREEN_OFF } +/// The type of screen state events coming from Android or iOS. +enum ScreenStateEvent { + SCREEN_UNLOCKED, + SCREEN_ON, + SCREEN_OFF; + + /// Returns the name of the enum value. + String get name { + switch (this) { + case ScreenStateEvent.SCREEN_UNLOCKED: + return Platform.isAndroid + ? 'android.intent.action.USER_PRESENT' + : 'SCREEN_UNLOCKED'; + case ScreenStateEvent.SCREEN_ON: + return Platform.isAndroid + ? 'android.intent.action.SCREEN_ON' + : 'SCREEN_ON'; + case ScreenStateEvent.SCREEN_OFF: + return Platform.isAndroid + ? 'android.intent.action.SCREEN_OFF' + : 'SCREEN_OFF'; + default: + throw new ArgumentError('Unknown ScreenStateEvent: $this'); + } + } + + /// Returns the enum value from the name. + static ScreenStateEvent fromName(String name) { + switch (name) { + case 'SCREEN_UNLOCKED': + case 'android.intent.action.USER_PRESENT': // Android only 'USER_PRESENT + return ScreenStateEvent.SCREEN_UNLOCKED; + case 'SCREEN_ON': + case 'android.intent.action.SCREEN_ON': // Android only 'SCREEN_ON' + return ScreenStateEvent.SCREEN_ON; + case 'SCREEN_OFF': + case 'android.intent.action.SCREEN_OFF': // Android only 'SCREEN_OFF' + return ScreenStateEvent.SCREEN_OFF; + default: + throw new ArgumentError('Unknown ScreenStateEvent: $name'); + } + } +} /// Custom Exception for the `screen_state` plugin, used whenever the plugin -/// is used on platforms other than Android class ScreenStateException implements Exception { String _cause; @@ -21,6 +61,7 @@ class Screen { static Screen? _singleton; EventChannel _eventChannel = const EventChannel('screenStateEvents'); Stream? _screenStateStream; + ScreenStateEvent? _lastScreenState; /// Constructs a singleton instance of [Screen]. /// @@ -31,29 +72,21 @@ class Screen { /// Stream of [ScreenStateEvent]s. /// Each event is streamed as it occurs on the phone. - /// Only Android [ScreenStateEvent] are streamed. - Stream? get screenStateStream { - if (Platform.isAndroid) { - if (_screenStateStream == null) { - _screenStateStream = _eventChannel - .receiveBroadcastStream() - .map((event) => _parseScreenStateEvent(event)); - } - return _screenStateStream; + Stream get screenStateStream { + if (!Platform.isAndroid && !Platform.isIOS) { + throw ScreenStateException( + 'Screen State API only available on Android and iOS.', + ); } - throw ScreenStateException('Screen State API only available on Android'); - } - ScreenStateEvent _parseScreenStateEvent(String event) { - switch (event) { - case 'android.intent.action.SCREEN_OFF': - return ScreenStateEvent.SCREEN_OFF; - case 'android.intent.action.SCREEN_ON': - return ScreenStateEvent.SCREEN_ON; - case 'android.intent.action.USER_PRESENT': - return ScreenStateEvent.SCREEN_UNLOCKED; - default: - throw new ArgumentError('$event was not recognized.'); + if (_screenStateStream == null) { + _screenStateStream = _eventChannel.receiveBroadcastStream().map( + (event) => ScreenStateEvent.fromName( + event, + ), + ); } + + return _screenStateStream!; } } diff --git a/packages/screen_state/pubspec.yaml b/packages/screen_state/pubspec.yaml index 0bd472dfd..fd0e9ebd4 100644 --- a/packages/screen_state/pubspec.yaml +++ b/packages/screen_state/pubspec.yaml @@ -1,6 +1,6 @@ name: screen_state -description: A plugin for reporting screen events while the flutter application is running in background. Works for Android only. -version: 3.0.1 +description: A plugin for reporting screen events while the flutter application is running in background. Works for Android and iOS only. +version: 4.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state environment: