diff --git a/README.md b/README.md index df141d5fe..548cf3bb3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ These are the available plugins in this repository. | [audio_streamer](./packages/audio_streamer) | Stream audio as PCM from mic| ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/audio_streamer.svg)](https://pub.dartlang.org/packages/audio_streamer) | | [mobility_features](./packages/mobility_features) | Compute daily mobility features from location data | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/mobility_features.svg)](https://pub.dartlang.org/packages/mobility_features) | | [carp_background_location](./packages/carp_background_location) | Track location, even when app is in the background | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/carp_background_location.svg)](https://pub.dartlang.org/packages/carp_background_location) | -| [flutter_foreground_service](./packages/flutter_foreground_service) | Foreground service for Android | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/flutter_foreground_service.svg)](https://pub.dartlang.org/packages/flutter_foreground_service) | +| [flutter_foreground_service](./packages/flutter_foreground_service) | Foreground service for Android | ✔️ | ❌ | [![pub package](https://img.shields.io/pub/v/flutter_foreground_service.svg)](https://pub.dartlang.org/packages/flutter_foreground_service) | ## Issues diff --git a/packages/activity_recognition_flutter/CHANGELOG.md b/packages/activity_recognition_flutter/CHANGELOG.md index df13d9fb4..d28e96bed 100644 --- a/packages/activity_recognition_flutter/CHANGELOG.md +++ b/packages/activity_recognition_flutter/CHANGELOG.md @@ -1,31 +1,45 @@ +## 5.0.0 + +- upgraded Android SDK level +- upgraded flutter version +- upgraded AGP + ## 4.2.0 -* small refactor and improvement of docs -* using `ActivityRecognition()` when creating a singleton -- standard practice in Dart. + +- small refactor and improvement of docs +- using `ActivityRecognition()` when creating a singleton -- standard practice in Dart. ## 4.1.0 -* [PR #474](https://github.com/cph-cachet/flutter-plugins/pull/474) - Android 12 intent-flag -* the name of the stream has been changed from `startStream` to `activityStream` -* cleanup in example app + +- [PR #474](https://github.com/cph-cachet/flutter-plugins/pull/474) - Android 12 intent-flag +- the name of the stream has been changed from `startStream` to `activityStream` +- cleanup in example app ## 4.0.5+1 -* [PR #408](https://github.com/cph-cachet/flutter-plugins/pull/408) + +- [PR #408](https://github.com/cph-cachet/flutter-plugins/pull/408) ## 4.0.4 -* improvements to documentation + +- improvements to documentation ## 4.0.3 -* [PR #358](https://github.com/cph-cachet/flutter-plugins/pull/358) + +- [PR #358](https://github.com/cph-cachet/flutter-plugins/pull/358) ## 4.0.2 -* [PR #302](https://github.com/cph-cachet/flutter-plugins/pull/302) -* [PR #351](https://github.com/cph-cachet/flutter-plugins/pull/351) + +- [PR #302](https://github.com/cph-cachet/flutter-plugins/pull/302) +- [PR #351](https://github.com/cph-cachet/flutter-plugins/pull/351) ## 4.0.1 -* Fix of issue #309, i.e. a null pointer that occurs when running the plugin on API 30. -* Replaced the deprecated `IntentService` with a `JobIntentService`. -* [PR #314](https://github.com/cph-cachet/flutter-plugins/pull/314) + +- Fix of issue #309, i.e. a null pointer that occurs when running the plugin on API 30. +- Replaced the deprecated `IntentService` with a `JobIntentService`. +- [PR #314](https://github.com/cph-cachet/flutter-plugins/pull/314) ## 4.0.0 + - Null safety migration - Updated swift code diff --git a/packages/activity_recognition_flutter/android/build.gradle b/packages/activity_recognition_flutter/android/build.gradle index 0a1316904..b2575a068 100644 --- a/packages/activity_recognition_flutter/android/build.gradle +++ b/packages/activity_recognition_flutter/android/build.gradle @@ -4,28 +4,28 @@ version '1.0' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { - minSdkVersion 21 + minSdkVersion 26 } lintOptions { disable 'InvalidPackage' diff --git a/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties b/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties index 01a286e96..ceccc3a85 100644 --- a/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/activity_recognition_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/activity_recognition_flutter/example/android/app/build.gradle b/packages/activity_recognition_flutter/example/android/app/build.gradle index 67e070f7f..c2be3c506 100644 --- a/packages/activity_recognition_flutter/example/android/app/build.gradle +++ b/packages/activity_recognition_flutter/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion flutter.compileSdkVersion lintOptions { disable 'InvalidPackage' @@ -34,8 +34,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dk.cachet.activity_recognition_flutter_example" - minSdkVersion 21 - targetSdkVersion 30 + minSdkVersion 26 + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml index d473144f0..53f30dd5e 100644 --- a/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/activity_recognition_flutter/example/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + android:windowSoftInputMode="adjustResize" + android:exported="true"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> diff --git a/packages/air_quality/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/air_quality/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt index 1656503f3..ac81bae64 100644 --- a/packages/air_quality/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt +++ b/packages/air_quality/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -1,12 +1,5 @@ package com.example.example -import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant -class MainActivity: FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine); - } -} +class MainActivity : FlutterActivity() diff --git a/packages/air_quality/example/android/build.gradle b/packages/air_quality/example/android/build.gradle index 3100ad2d5..94a38e717 100644 --- a/packages/air_quality/example/android/build.gradle +++ b/packages/air_quality/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/air_quality/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/air_quality/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..c021028dd 100644 --- a/packages/air_quality/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/air_quality/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/air_quality/example/ios/Flutter/AppFrameworkInfo.plist b/packages/air_quality/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a..4f8d4d245 100644 --- a/packages/air_quality/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/air_quality/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/packages/air_quality/example/ios/Runner.xcodeproj/project.pbxproj b/packages/air_quality/example/ios/Runner.xcodeproj/project.pbxproj index 252c7539f..fb63202ef 100644 --- a/packages/air_quality/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/air_quality/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -135,7 +135,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -179,10 +179,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -193,6 +195,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -280,7 +283,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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -362,7 +365,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 = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -411,7 +414,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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/air_quality/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/air_quality/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..3db53b6e1 100644 --- a/packages/air_quality/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/air_quality/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/air_quality/pubspec.yaml b/packages/air_quality/pubspec.yaml index a46920ec9..c84fe3601 100644 --- a/packages/air_quality/pubspec.yaml +++ b/packages/air_quality/pubspec.yaml @@ -1,15 +1,15 @@ name: air_quality description: Air quality index from the World's Air Quality Index (WAQI) service. -version: 2.0.0 +version: 4.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/air_quality environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: flutter: sdk: flutter - http: ^0.13.1 + http: ^1.1.0 dev_dependencies: flutter_test: diff --git a/packages/app_usage/CHANGELOG.md b/packages/app_usage/CHANGELOG.md index 30d5db8e2..a55d13b08 100644 --- a/packages/app_usage/CHANGELOG.md +++ b/packages/app_usage/CHANGELOG.md @@ -1,3 +1,14 @@ +## 3.0.1 + +- Updated pubsec.yaml description +- Fixed license file + +## 3.0.0 + +- Updates Kotlin plugin and AGP +- Upgrade of compileSdkVersion +- Upgrade to Dart 3 + ## 2.1.1 - check of Android OS version added when getting `lastForeground`. diff --git a/packages/app_usage/LICENSE b/packages/app_usage/LICENSE index 12dd59ebb..67a247a2c 100644 --- a/packages/app_usage/LICENSE +++ b/packages/app_usage/LICENSE @@ -1,6 +1,6 @@ MIT License. -Copyright 2018-2022 Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). +Copyright 2019 Copenhagen Center for Health Technology (CACHET) at 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 @@ -14,3 +14,4 @@ THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I 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/app_usage/android/build.gradle b/packages/app_usage/android/build.gradle index 9212c2e75..894740210 100644 --- a/packages/app_usage/android/build.gradle +++ b/packages/app_usage/android/build.gradle @@ -2,15 +2,15 @@ group 'dk.cachet.app_usage' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' +ext.kotlin_version = '1.7.10' repositories { google() - jcenter() +mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/app_usage/android/gradle/wrapper/gradle-wrapper.properties b/packages/app_usage/android/gradle/wrapper/gradle-wrapper.properties index 01a286e96..1412e4a0a 100644 --- a/packages/app_usage/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/app_usage/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/app_usage/example/.flutter-plugins-dependencies b/packages/app_usage/example/.flutter-plugins-dependencies index 2307f6224..8869a5276 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/bardram/dev/flutter-plugins/packages/app_usage/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"app_usage","dependencies":[]}],"date_created":"2022-12-05 22:39:14.995165","version":"3.3.9"} \ 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/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 diff --git a/packages/app_usage/example/android/app/build.gradle b/packages/app_usage/example/android/app/build.gradle index bc9b7a787..1b852926f 100644 --- a/packages/app_usage/example/android/app/build.gradle +++ b/packages/app_usage/example/android/app/build.gradle @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion flutter.compileSdkVersion + sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +40,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dk.cachet.app_usage_example" - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/app_usage/example/android/app/src/main/AndroidManifest.xml b/packages/app_usage/example/android/app/src/main/AndroidManifest.xml index 23161d8e3..4b22c33ba 100644 --- a/packages/app_usage/example/android/app/src/main/AndroidManifest.xml +++ b/packages/app_usage/example/android/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" + android:exported="true" android:windowSoftInputMode="adjustResize"> + - - - - + + ``` -**NOTE** In Android 11 location permissions cannot be set automatically by the app (via the manifest file). Google writes on [this page](https://developer.android.com/training/location/permissions#request-background-location) that: - +**NOTE** In Android 11 and above location permissions cannot be set automatically by the app (via the manifest file). Google writes on [this page](https://developer.android.com/training/location/permissions#request-background-location) that: > On Android 11 (API level 30) and higher [...] the system dialog doesn’t include the **Allow all the time** option. Instead, users must enable background location on a settings page, as shown in figure 7. @@ -67,25 +55,23 @@ Add the following entries to your `Info.plist` file This app needs access to location when open. UIBackgroundModes - location + location ``` -Next, overwrite your AppDelegate.swift in the XCode project with: +Next, overwrite your `AppDelegate.swift` with: ```swift import UIKit import Flutter - -import background_locator +import background_locator_2 func registerPlugins(registry: FlutterPluginRegistry) -> () { if (!registry.hasPlugin("BackgroundLocatorPlugin")) { GeneratedPluginRegistrant.register(with: registry) - } + } } - @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( @@ -114,7 +100,7 @@ func registerPlugins(registry: FlutterPluginRegistry) -> () { // start listen to location updates StreamSubscription locationSubscription = LocationManager() .locationStream - .listen((LocationDto dto) => print(dto)); + .listen((LocationDto loc) => print(loc)); // cancel listening and stop the location manager locationSubscription?.cancel(); @@ -133,5 +119,3 @@ Please file feature requests and bug reports at the [issue tracker][tracker]. This software is copyright (c) [Copenhagen Center for Health Technology (CACHET)](https://www.cachet.dk/) at the [Technical University of Denmark (DTU)](https://www.dtu.dk). This software is available 'as-is' under a [MIT license](https://github.com/cph-cachet/flutter-plugins/blob/master/packages/carp_background_location/LICENSE). - - diff --git a/packages/carp_background_location/example/android/app/build.gradle b/packages/carp_background_location/example/android/app/build.gradle index dcb69350b..dcf202e60 100644 --- a/packages/carp_background_location/example/android/app/build.gradle +++ b/packages/carp_background_location/example/android/app/build.gradle @@ -26,21 +26,18 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility 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 16 - targetSdkVersion 30 + minSdkVersion 19 + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/carp_background_location/example/android/app/src/main/AndroidManifest.xml b/packages/carp_background_location/example/android/app/src/main/AndroidManifest.xml index 4a8a4927e..0f857beca 100644 --- a/packages/carp_background_location/example/android/app/src/main/AndroidManifest.xml +++ b/packages/carp_background_location/example/android/app/src/main/AndroidManifest.xml @@ -15,11 +15,12 @@ - - - - - - - - - - + android:exported="true" /> + android:exported="true" /> --> - + + + + + + + diff --git a/packages/carp_background_location/example/android/build.gradle b/packages/carp_background_location/example/android/build.gradle index 3100ad2d5..77611697e 100644 --- a/packages/carp_background_location/example/android/build.gradle +++ b/packages/carp_background_location/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.8.0' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/carp_background_location/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/carp_background_location/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..cfe88f690 100644 --- a/packages/carp_background_location/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/carp_background_location/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist b/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a..4f8d4d245 100644 --- a/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/packages/carp_background_location/example/ios/Podfile b/packages/carp_background_location/example/ios/Podfile index 1e8c3c90a..88359b225 100644 --- a/packages/carp_background_location/example/ios/Podfile +++ b/packages/carp_background_location/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj b/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj index 3cbc86c9d..9ce43b052 100644 --- a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/carp_background_location/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 = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -207,10 +207,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -243,6 +245,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -308,7 +311,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -348,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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -389,7 +391,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -435,7 +436,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 = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -445,7 +446,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -485,7 +485,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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -503,7 +503,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H33599VJ27; + DEVELOPMENT_TEAM = 8TB3T6MAZG; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -518,7 +518,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = "dk.cachet.carp-background-location"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; diff --git a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/carp_background_location/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/carp_background_location/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..3db53b6e1 100644 --- a/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ () { if (!registry.hasPlugin("BackgroundLocatorPlugin")) { GeneratedPluginRegistrant.register(with: registry) - } + } } - @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( @@ -20,4 +18,4 @@ func registerPlugins(registry: FlutterPluginRegistry) -> () { BackgroundLocatorPlugin.setPluginRegistrantCallback(registerPlugins) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} \ No newline at end of file +} diff --git a/packages/carp_background_location/example/ios/Runner/Info.plist b/packages/carp_background_location/example/ios/Runner/Info.plist index 67a8621d2..c90d59f48 100644 --- a/packages/carp_background_location/example/ios/Runner/Info.plist +++ b/packages/carp_background_location/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -28,6 +30,8 @@ This app needs access to location when in the background. NSLocationWhenInUseUsageDescription This app needs access to location when open. + UIApplicationSupportsIndirectInputEvents + UIBackgroundModes location diff --git a/packages/carp_background_location/example/lib/main.dart b/packages/carp_background_location/example/lib/main.dart index cbcd995ee..51d7afc26 100644 --- a/packages/carp_background_location/example/lib/main.dart +++ b/packages/carp_background_location/example/lib/main.dart @@ -12,31 +12,9 @@ class MyApp extends StatefulWidget { enum LocationStatus { UNKNOWN, INITIALIZED, RUNNING, STOPPED } -String dtoToString(LocationDto dto) => - 'Location ${dto.latitude}, ${dto.longitude} at ${DateTime.fromMillisecondsSinceEpoch(dto.time ~/ 1)}'; - -Widget dtoWidget(LocationDto? dto) { - if (dto == null) - return Text("No location yet"); - else - return Column( - children: [ - Text( - '${dto.latitude}, ${dto.longitude}', - ), - Text( - '@', - ), - Text('${DateTime.fromMillisecondsSinceEpoch(dto.time ~/ 1)}') - ], - ); -} - class _MyAppState extends State { String logStr = ''; - LocationDto? lastLocation; - DateTime? lastTimeLocation; - Stream? locationStream; + LocationDto? _lastLocation; StreamSubscription? locationSubscription; LocationStatus _status = LocationStatus.UNKNOWN; @@ -48,7 +26,6 @@ class _MyAppState extends State { LocationManager().distanceFilter = 0; LocationManager().notificationTitle = 'CARP Location Example'; LocationManager().notificationMsg = 'CARP is tracking your location'; - locationStream = LocationManager().locationStream; _status = LocationStatus.INITIALIZED; } @@ -56,12 +33,10 @@ class _MyAppState extends State { void getCurrentLocation() async => onData(await LocationManager().getCurrentLocation()); - void onData(LocationDto dto) { - // print(dtoToString(dto)); - print(dto); + void onData(LocationDto location) { + print('>> $location'); setState(() { - lastLocation = dto; - lastTimeLocation = DateTime.now(); + _lastLocation = location; }); } @@ -70,7 +45,7 @@ class _MyAppState extends State { await Permission.locationAlways.isGranted; /// Tries to ask for "location always" permissions from the user. - /// Returns `true` if successful, `false` othervise. + /// Returns `true` if successful, `false` otherwise. Future askForLocationAlwaysPermission() async { bool granted = await Permission.locationAlways.isGranted; @@ -89,7 +64,7 @@ class _MyAppState extends State { await askForLocationAlwaysPermission(); locationSubscription?.cancel(); - locationSubscription = locationStream?.listen(onData); + locationSubscription = LocationManager().locationStream.listen(onData); await LocationManager().start(); setState(() { _status = LocationStatus.RUNNING; @@ -120,13 +95,7 @@ class _MyAppState extends State { ), ); - Widget status() => Text("Status: ${_status.toString().split('.').last}"); - - Widget lastLoc() => Text( - lastLocation != null - ? dtoToString(lastLocation!) - : 'Unknown last location', - textAlign: TextAlign.center); + Widget statusText() => Text("Status: ${_status.toString().split('.').last}"); Widget currentLocationButton() => SizedBox( width: double.maxFinite, @@ -136,6 +105,24 @@ class _MyAppState extends State { ), ); + Widget locationWidget() { + if (_lastLocation == null) + return Text("No location yet"); + else + return Column( + children: [ + Text( + '${_lastLocation!.latitude}, ${_lastLocation!.longitude}', + ), + Text( + '@', + ), + Text( + '${DateTime.fromMillisecondsSinceEpoch(_lastLocation!.time ~/ 1)}') + ], + ); + } + @override void dispose() => super.dispose(); @@ -157,9 +144,9 @@ class _MyAppState extends State { stopButton(), currentLocationButton(), Divider(), - status(), + statusText(), Divider(), - dtoWidget(lastLocation), + locationWidget(), ], ), ), diff --git a/packages/carp_background_location/example/pubspec.yaml b/packages/carp_background_location/example/pubspec.yaml index 355604639..45bb99421 100644 --- a/packages/carp_background_location/example/pubspec.yaml +++ b/packages/carp_background_location/example/pubspec.yaml @@ -1,24 +1,11 @@ -name: example -description: A new Flutter project. - -# 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 - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+1 +name: carp_background_location_example +description: A example app for the CARP Background Location plugin. +publish_to: 'none' +version: 4.0.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -27,53 +14,9 @@ dependencies: path: ../ permission_handler: ^8.1.0 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - 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 diff --git a/packages/carp_background_location/example/test/widget_test.dart b/packages/carp_background_location/example/test/widget_test.dart deleted file mode 100644 index 747db1da3..000000000 --- a/packages/carp_background_location/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/packages/carp_background_location/lib/carp_background_location.dart b/packages/carp_background_location/lib/carp_background_location.dart index f9b3f5800..6d9c48b40 100644 --- a/packages/carp_background_location/lib/carp_background_location.dart +++ b/packages/carp_background_location/lib/carp_background_location.dart @@ -3,16 +3,18 @@ library carp_background_location; import 'dart:async'; import 'dart:isolate'; import 'dart:ui'; -import 'dart:math'; +// import 'dart:math'; -import 'package:background_locator/background_locator.dart'; -import 'package:background_locator/location_dto.dart'; -import 'package:background_locator/settings/android_settings.dart'; -import 'package:background_locator/settings/ios_settings.dart'; -import 'package:background_locator/settings/locator_settings.dart'; +import 'package:background_locator_2/background_locator.dart'; +import 'package:background_locator_2/location_dto.dart'; +import 'package:background_locator_2/settings/android_settings.dart'; +import 'package:background_locator_2/settings/ios_settings.dart'; +import 'package:background_locator_2/settings/locator_settings.dart'; -export 'package:background_locator/location_dto.dart'; -export 'package:background_locator/settings/locator_settings.dart'; +export 'package:background_locator_2/location_dto.dart'; +export 'package:background_locator_2/settings/android_settings.dart'; +export 'package:background_locator_2/settings/ios_settings.dart'; +export 'package:background_locator_2/settings/locator_settings.dart'; part 'src/location_callback_handler.dart'; part 'src/location_service_repository.dart'; diff --git a/packages/carp_background_location/lib/src/location_callback_handler.dart b/packages/carp_background_location/lib/src/location_callback_handler.dart index 20bf72a5c..3dae68eb9 100644 --- a/packages/carp_background_location/lib/src/location_callback_handler.dart +++ b/packages/carp_background_location/lib/src/location_callback_handler.dart @@ -1,23 +1,19 @@ part of carp_background_location; +@pragma('vm:entry-point') class LocationCallbackHandler { - static Future initCallback(Map params) async { - LocationServiceRepository myLocationCallbackRepository = - LocationServiceRepository(); - await myLocationCallbackRepository.init(params); - } + @pragma('vm:entry-point') + static Future initCallback(Map params) async => + await LocationServiceRepository().init(params); - static Future disposeCallback() async { - LocationServiceRepository myLocationCallbackRepository = - LocationServiceRepository(); - await myLocationCallbackRepository.dispose(); - } + @pragma('vm:entry-point') + static Future disposeCallback() async => + await LocationServiceRepository().dispose(); - static Future callback(LocationDto locationDto) async { - LocationServiceRepository myLocationCallbackRepository = - LocationServiceRepository(); - await myLocationCallbackRepository.callback(locationDto); - } + @pragma('vm:entry-point') + static Future callback(LocationDto locationDto) async => + await LocationServiceRepository().callback(locationDto); + @pragma('vm:entry-point') static Future notificationCallback() async {} } diff --git a/packages/carp_background_location/lib/src/location_manager.dart b/packages/carp_background_location/lib/src/location_manager.dart index db610bda0..4cce9fd2b 100644 --- a/packages/carp_background_location/lib/src/location_manager.dart +++ b/packages/carp_background_location/lib/src/location_manager.dart @@ -52,7 +52,7 @@ class LocationManager { Stream dataStream = _port.asBroadcastStream(); _locationStream = dataStream .where((event) => event != null) - .map((location) => location as LocationDto); + .map((json) => LocationDto.fromJson(json)); } return _locationStream!; } @@ -77,6 +77,8 @@ class LocationManager { await BackgroundLocator.registerLocationUpdate( LocationCallbackHandler.callback, + initCallback: LocationCallbackHandler.initCallback, + disposeCallback: LocationCallbackHandler.disposeCallback, autoStop: false, androidSettings: AndroidSettings( accuracy: _accuracy, @@ -103,24 +105,24 @@ class LocationManager { /// Set the title of the notification for the background service. /// Android only. - set notificationTitle(value) => _notificationTitle = value; + set notificationTitle(String title) => _notificationTitle = title; /// Set the message of the notification for the background service. /// Android only. - set notificationMsg(value) => _notificationMsg = value; + set notificationMsg(String message) => _notificationMsg = message; /// Set the long message of the notification for the background service. /// Android only. - set notificationBigMsg(value) => _notificationBigMsg = value; + set notificationBigMsg(String message) => _notificationBigMsg = message; /// Set the update interval in seconds. /// Android only. - set interval(int value) => _interval = value; + set interval(int interval) => _interval = interval; /// Set the update distance, i.e. the distance the user needs to move /// before an update is fired. - set distanceFilter(double value) => _distanceFilter = value; + set distanceFilter(double distance) => _distanceFilter = distance; /// Set the update accuracy. See [LocationAccuracy] for options. - set accuracy(LocationAccuracy value) => _accuracy = value; + set accuracy(LocationAccuracy accuracy) => _accuracy = accuracy; } diff --git a/packages/carp_background_location/lib/src/location_service_repository.dart b/packages/carp_background_location/lib/src/location_service_repository.dart index 0c02c85f0..0baa75200 100644 --- a/packages/carp_background_location/lib/src/location_service_repository.dart +++ b/packages/carp_background_location/lib/src/location_service_repository.dart @@ -1,37 +1,18 @@ part of carp_background_location; class LocationServiceRepository { + static const String isolateName = 'LocatorIsolate'; static LocationServiceRepository _instance = LocationServiceRepository._(); - LocationServiceRepository._(); - factory LocationServiceRepository() => _instance; - static const String isolateName = 'LocatorIsolate'; - - Future init(Map params) async { - final SendPort? send = IsolateNameServer.lookupPortByName(isolateName); - send?.send(null); - } - - Future dispose() async { - final SendPort? send = IsolateNameServer.lookupPortByName(isolateName); - send?.send(null); - } - - Future callback(LocationDto locationDto) async { - final SendPort? send = IsolateNameServer.lookupPortByName(isolateName); - send?.send(locationDto); - } - - static double dp(double val, int places) { - double mod = pow(10.0, places) as double; - return ((val * mod).round().toDouble() / mod); - } + Future init(Map params) async => + IsolateNameServer.lookupPortByName(isolateName)?.send(null); - static String formatDateLog(DateTime date) => - '${date.hour.toString()}:${date.minute.toString()}:${date.second.toString()}'; + Future dispose() async => + IsolateNameServer.lookupPortByName(isolateName)?.send(null); - static String formatLog(LocationDto locationDto) => - '${dp(locationDto.latitude, 4).toString()} ${dp(locationDto.longitude, 4).toString()}'; + Future callback(LocationDto locationDto) async => + IsolateNameServer.lookupPortByName(isolateName) + ?.send(locationDto.toJson()); } diff --git a/packages/carp_background_location/pubspec.yaml b/packages/carp_background_location/pubspec.yaml index df1e1c93b..dd6e4fd57 100644 --- a/packages/carp_background_location/pubspec.yaml +++ b/packages/carp_background_location/pubspec.yaml @@ -1,21 +1,20 @@ name: carp_background_location description: A location plugin that works in the background. Supports Android and iOS -version: 3.0.1 +version: 4.0.0 homepage: https://github.com/cph-cachet/flutter-plugins -author: CACHET Team environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.17.0 <4.0.0" dependencies: flutter: sdk: flutter - background_locator: ^1.6.6 + background_locator_2: ^2.0.6 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 + flutter_lints: any flutter: diff --git a/packages/esense_flutter/CHANGELOG.md b/packages/esense_flutter/CHANGELOG.md index 54c3c7a74..28ec5b406 100644 --- a/packages/esense_flutter/CHANGELOG.md +++ b/packages/esense_flutter/CHANGELOG.md @@ -1,54 +1,80 @@ +## 1.0.0 + +- first stable release +- update to Bluetooth permissions in demo app +- improved handling of disconnection + +## 0.7.0 + +- Upgrade of Kotlin and AGP +- Dart fix + ## 0.6.2 -* Added podspec file to iOS + +- Added podspec file to iOS ## 0.6.1 -* fixed bug in sampling rate configuration on Android -* small improvement to API doc and example app + +- fixed bug in sampling rate configuration on Android +- small improvement to API doc and example app ## 0.6.0 -* moved permission handling to app level -* improved on iOS implementation -* improvement to exampel app -* improvement to README + +- moved permission handling to app level +- improved on iOS implementation +- improvement to example app +- improvement to README ## 0.5.0 -* upgrade to Android embedding v2 + +- upgrade to Android embedding v2 ## 0.4.3 -* fix of error in setSamplingRate method on iOS + +- fix of error in setSamplingRate method on iOS ## 0.4.0 -* upgrade to null-safety + +- upgrade to null-safety ## 0.3.0 -* singleton changed to recommended dart style using `ESenseManager()`. -* fixed bug in reconnection. -* updated the Android app to [post 1.12 Android project](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects). -* small improvement to the demo app (now supports connection via a button). + +- singleton changed to recommended dart style using `ESenseManager()`. +- fixed bug in reconnection. +- updated the Android app to [post 1.12 Android project](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects). +- small improvement to the demo app (now supports connection via a button). ## 0.2.1 -* updated Swift implementation to version 5 + +- updated Swift implementation to version 5 ## 0.2.0+2 -* update to readme on iOS -* added bluetooth permission to `Info.plist` + +- update to readme on iOS +- added bluetooth permission to `Info.plist` ## 0.2.0+1 -* small update to documentation + +- small update to documentation ## 0.2.0 -* support for iOS implemented -* improved example app -* improvement to documentation + +- support for iOS implemented +- improved example app +- improvement to documentation ## 0.1.3 -* sampling rate can be set before connecting to device. + +- sampling rate can be set before connecting to device. ## 0.1.2 -* small improvement to the example app (still not very good though...) + +- small improvement to the example app (still not very good though...) ## 0.1.1+1 -* update of readme documentation on API. + +- update of readme documentation on API. ## 0.1.0 -* Initial release supporting 1st released version of the Android eSense library, dated April 2019. + +- Initial release supporting 1st released version of the Android eSense library, dated April 2019. diff --git a/packages/esense_flutter/README.md b/packages/esense_flutter/README.md index be19f92ed..6c2ff5543 100644 --- a/packages/esense_flutter/README.md +++ b/packages/esense_flutter/README.md @@ -2,38 +2,42 @@ This plugin supports the [eSense](https://www.esense.io) earable computing platform on both Android and iOS. - [![pub package](https://img.shields.io/pub/v/esense_flutter.svg)](https://pub.dartlang.org/packages/esense_flutter) ## Install (Flutter) + Add `esense_flutter` as a dependency in `pubspec.yaml`. For help on adding as a dependency, view the [pubspec documenation](https://flutter.io/using-packages/). -## Android -The package uses your location and bluetooth to fetch data from the eSense ear plugs. -Therefore location tracking and bluetooth must be enabled. +## Android -Add the following entry to your `manifest.xml` file, in the Android project of your application: +The package uses bluetooth to fetch data from the eSense earplugs. +Therefore permission to access bluetooth must be enabled. + +Add the following entry to your `manifest.xml` file, in the Android part of your application: ```xml - - - - - + + + + ``` -Also make sure to obtain permissions in your app to use location and bluetooth. +Also make sure to obtain permissions in your app to use bluetooth. See the example app on how to e.g. use the [`permission_handler`](https://pub.dev/packages/permission_handler) for this. Note that the plugin **does not** handle permissions - this has to be done on an app level. -Set the Android compile and minimum SDK versions to `compileSdkVersion 28`, +Set the Android compile and minimum SDK versions to `compileSdkVersion 28`, and `minSdkVersion 23` respectively, inside the `android/app/build.gradle` file. ## iOS -Requires iOS 10 or later. Hence, in your `Podfile` in the `ios` folder of your app, -make sure that the platform is set to `10.0`. - +Requires iOS 10 or later. Hence, in your `Podfile` in the `ios` folder of your app, make sure that the platform is set to `10.0`. ``` platform :ios, '10.0' @@ -46,32 +50,29 @@ Add this permission in the `Info.plist` file located in `ios/Runner`: Uses bluetooth to connect to the eSense device UIBackgroundModes - bluetooth-central - bluetooth-peripheral + bluetooth-central + bluetooth-peripheral audio external-accessory fetch - ``` -Note that on iOS, connecting to the eSense device may take several seconds. +Note that on iOS, connecting to the eSense device may take several seconds. ## Usage -The eSense Flutter plugin has been designed to resemble the Android eSense API almost __1:1__. Hence, you should be able to recognize the names of the different classes and class variables. -For example, the methods on the [`ESenseManager`](https://pub.dev/documentation/esense/latest/esense/ESenseManager-class.html) class is mapped 1:1. +The eSense Flutter plugin has been designed to resemble the Android eSense API almost **1:1**. Hence, you should be able to recognize the names of the different classes and class variables. +For example, the methods on the [`ESenseManager`](https://pub.dev/documentation/esense/latest/esense/ESenseManager-class.html) class is mapped 1:1. See the [eSense Android documentation](https://www.esense.io/share/eSense-Android-Library.pdf) on how it all works. However, one major design change has been done; this eSense Flutter plugin complies to the Dart/Flutter reactive programming architecture using [Stream](https://api.dartlang.org/stable/2.4.0/dart-async/Stream-class.html)s. -Hence, you do not 'add listerners' to an eSense device (as you do in Java) -- rather, you obtain a Dart stream and listen to this stream (and exploit all the [other very nice stream operations](https://dart.dev/tutorials/language/streams) which are available in Dart). -Below, we shall describe how to use the eSense streams. +Hence, you do not 'add listeners' to an eSense device (as you do in Java) -- rather, you obtain a Dart stream and listen to this stream (and exploit all the [other very nice stream operations](https://dart.dev/tutorials/language/streams) which are available in Dart). +Below, we shall describe how to use the eSense streams. But first -- let's see how to set up and connect to an eSense device in the first place. Note that playing and recording audio are performed via the Bluetooth Classic interface and are not supported by the eSense library described here. - - ### Setting up and Connecting to an eSense Device All operations on the eSense device happens via the [`ESenseManager`](https://pub.dev/documentation/esense/latest/esense/ESenseManager-class.html). @@ -99,11 +100,11 @@ bool connecting = await eSenseManager.connect(); ``` Everything with the eSense API happens asynchronously. Hence, the `connect` call merely initiates the connection -process. In order to know the status of the connection process (successful or not), you should listen to +process. In order to know the status of the connection process (successful or not), you should listen to connection events ([`ConnectionEvent`](https://pub.dev/documentation/esense/latest/esense/ConnectionEvent-class.html)). This is done via the [`connectionEvents`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/connectionEvents.html) stream. Note, that if you want to know if your connection to the device is successful, you should initiate listening -__before__ the connection is initiated, as shown above. +**before** the connection is initiated, as shown above. ### Listen to Sensor Events @@ -129,7 +130,7 @@ subscription = eSenseManager.sensorEvents.listen((event) { ### Read eSense Device Events -Reading properties of the eSense device happens asynchronously. Hence, in order to obtain properties, you should +Reading properties of the eSense device happens asynchronously. Hence, in order to obtain properties, you should do two things: 1. listen to the [`ESenseManager.eSenseEvents`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/eSenseEvents.html) stream @@ -149,22 +150,21 @@ eSenseManager.getDeviceName(); When the button on the eSense device is pressed, the `eSenseEvents` stream will send an [`ButtonEventChanged`](https://pub.dev/documentation/esense/latest/esense/ButtonEventChanged-class.html) event. - ### Change the Configuration of the eSense Device -The [`ESenseManager`](https://pub.dev/documentation/esense/latest/esense/ESenseManager-class.html) exposes methods -to change the configuration of the eSense device. -With the plugin, you can change the device name using [`setDeviceName()`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/setDeviceName.html), -change the advertising and connection interval using [`setAdvertisementAndConnectiontInterval()`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/setAdvertisementAndConnectiontInterval.html), +The [`ESenseManager`](https://pub.dev/documentation/esense/latest/esense/ESenseManager-class.html) exposes methods +to change the configuration of the eSense device. +With the plugin, you can change the device name using [`setDeviceName()`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/setDeviceName.html), +change the advertising and connection interval using [`setAdvertisementAndConnectiontInterval()`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/setAdvertisementAndConnectiontInterval.html), and change the IMU sensor configuration using [`setSensorConfig()`](https://pub.dev/documentation/esense/latest/esense/ESenseManager/setSensorConfig.html). -__Note:__ At the time of writing, the `setSensorConfig()` method is _not_ implemented. +**Note:** At the time of writing, the `setSensorConfig()` method is _not_ implemented. ### Limitations in the eSense BTLE interface -Note that there is a limitation to the eSense BTLE interface which implie that you __should not__ +Note that there is a limitation to the eSense BTLE interface which implie that you **should not** invoke methods on the ESenseManager in a fast pace after each other. -For example, the following code __will not work__: +For example, the following code **will not work**: `````dart // set up a event listener @@ -197,6 +197,5 @@ Timer(Duration(seconds: 5), () async => await eSenseManager.getSensorConfig()); ## Authors - * [Jakob E. Bardram](https://www.bardram.net) Technical University of Denmark - * The iOS implementation uses the [eSense iOS Library](https://github.com/tetujin/ESense). - +* [Jakob E. Bardram](https://www.bardram.net) Technical University of Denmark +* The iOS implementation uses the [eSense iOS Library](https://github.com/tetujin/ESense). diff --git a/packages/esense_flutter/analysis_options.yaml b/packages/esense_flutter/analysis_options.yaml index a5744c1cf..c9eb45285 100644 --- a/packages/esense_flutter/analysis_options.yaml +++ b/packages/esense_flutter/analysis_options.yaml @@ -1,4 +1,18 @@ -include: package:flutter_lints/flutter.yaml - # 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 diff --git a/packages/esense_flutter/example/android/app/build.gradle b/packages/esense_flutter/example/android/app/build.gradle index 29186428d..2043a4db3 100644 --- a/packages/esense_flutter/example/android/app/build.gradle +++ b/packages/esense_flutter/example/android/app/build.gradle @@ -36,7 +36,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dk.cachet.esense_flutter_example" - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/esense_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/esense_flutter/example/android/app/src/main/AndroidManifest.xml index bd5dcc8ef..eaf22b319 100644 --- a/packages/esense_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/esense_flutter/example/android/app/src/main/AndroidManifest.xml @@ -3,11 +3,22 @@ xmlns:tools="http://schemas.android.com/tools"> - + + + + + + CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/esense_flutter/example/ios/Podfile b/packages/esense_flutter/example/ios/Podfile index 9411102b1..313ea4a15 100644 --- a/packages/esense_flutter/example/ios/Podfile +++ b/packages/esense_flutter/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/esense_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/esense_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 9064f704d..281b0b2a7 100644 --- a/packages/esense_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/esense_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -238,10 +238,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -252,6 +254,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -339,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -355,6 +358,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -416,7 +420,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -465,7 +469,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -483,7 +487,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8TB3T6MAZG; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -506,6 +510,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 59TCTNUBMQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/packages/esense_flutter/example/ios/Runner/Info.plist b/packages/esense_flutter/example/ios/Runner/Info.plist index 1283378c8..6c06cebd2 100644 --- a/packages/esense_flutter/example/ios/Runner/Info.plist +++ b/packages/esense_flutter/example/ios/Runner/Info.plist @@ -51,5 +51,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/esense_flutter/example/lib/main.dart b/packages/esense_flutter/example/lib/main.dart index 89d1561b6..42d2f9c35 100644 --- a/packages/esense_flutter/example/lib/main.dart +++ b/packages/esense_flutter/example/lib/main.dart @@ -5,9 +5,11 @@ import 'dart:async'; import 'package:esense_flutter/esense.dart'; import 'package:permission_handler/permission_handler.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override _MyAppState createState() => _MyAppState(); } @@ -33,19 +35,22 @@ class _MyAppState extends State { } Future _askForPermissions() async { - if (!(await Permission.bluetooth.request().isGranted)) { + if (!(await Permission.bluetoothScan.request().isGranted && + await Permission.bluetoothConnect.request().isGranted)) { print( 'WARNING - no permission to use Bluetooth granted. Cannot access eSense device.'); } - if (!(await Permission.locationWhenInUse.request().isGranted)) { - print( - 'WARNING - no permission to access location granted. Cannot access eSense device.'); + // for some strange reason, Android requires permission to location for Bluetooth to work.....? + if (Platform.isAndroid) { + if (!(await Permission.locationWhenInUse.request().isGranted)) { + print( + 'WARNING - no permission to access location granted. Cannot access eSense device.'); + } } } Future _listenToESense() async { - // for some strange reason, Android requires permission to location for the eSense to work???? - if (Platform.isAndroid) await _askForPermissions(); + await _askForPermissions(); // if you want to get the connection events when connecting, // set up the listener BEFORE connecting... @@ -67,6 +72,7 @@ class _MyAppState extends State { break; case ConnectionType.disconnected: _deviceStatus = 'disconnected'; + sampling = false; break; case ConnectionType.device_found: _deviceStatus = 'device_found'; @@ -81,7 +87,7 @@ class _MyAppState extends State { Future _connectToESense() async { if (!connected) { - print('connecting...'); + print('Trying to connect to eSense device...'); connected = await eSenseManager.connect(); setState(() { @@ -142,15 +148,15 @@ class _MyAppState extends State { const Duration(seconds: 4), () async => await eSenseManager.getAdvertisementAndConnectionInterval()); - Timer(const Duration(seconds: 5), + Timer(const Duration(seconds: 15), () async => await eSenseManager.getSensorConfig()); } StreamSubscription? subscription; void _startListenToSensorEvents() async { - // any changes to the sampling frequency must be done BEFORE listening to sensor events - print('setting sampling frequency...'); - await eSenseManager.setSamplingRate(10); + // // any changes to the sampling frequency must be done BEFORE listening to sensor events + // print('setting sampling frequency...'); + // await eSenseManager.setSamplingRate(10); // subscribe to sensor event from the eSense device subscription = eSenseManager.sensorEvents.listen((event) { diff --git a/packages/esense_flutter/example/pubspec.yaml b/packages/esense_flutter/example/pubspec.yaml index 8345bf5c9..e5572d449 100644 --- a/packages/esense_flutter/example/pubspec.yaml +++ b/packages/esense_flutter/example/pubspec.yaml @@ -30,8 +30,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - # The eSense plugin requires persmissions to location and bluetooth. - permission_handler: ^9.2.0 + # The eSense plugin requires permissions to location and bluetooth. + permission_handler: ^10.3.0 dev_dependencies: flutter_test: diff --git a/packages/esense_flutter/lib/esense.dart b/packages/esense_flutter/lib/esense.dart index c94a1857c..3a5434328 100644 --- a/packages/esense_flutter/lib/esense.dart +++ b/packages/esense_flutter/lib/esense.dart @@ -22,12 +22,13 @@ class ESenseManager { static const String ESenseSensorEventChannelName = 'esense.io/esense_sensor'; final MethodChannel _eSenseManagerMethodChannel = - MethodChannel(ESenseManagerMethodChannelName); + const MethodChannel(ESenseManagerMethodChannelName); final EventChannel _eSenseConnectionEventChannel = - EventChannel(ESenseConnectionEventChannelName); - final EventChannel _eSenseEventChannel = EventChannel(ESenseEventChannelName); + const EventChannel(ESenseConnectionEventChannelName); + final EventChannel _eSenseEventChannel = + const EventChannel(ESenseEventChannelName); final EventChannel _eSenseSensorEventChannel = - EventChannel(ESenseSensorEventChannelName); + const EventChannel(ESenseSensorEventChannelName); Stream? _connectionEventStream; Stream? _eventStream; @@ -46,7 +47,7 @@ class ESenseManager { /// Constructs an eSense manager for a device with name [deviceName]. ESenseManager(this.deviceName) { - assert(deviceName.length > 0, + assert(deviceName.isNotEmpty, 'Must provide a valid name of the eSense device to connect to.'); } @@ -72,8 +73,9 @@ class ESenseManager { _eventStream = null; _sensorStream = null; - return await _eSenseManagerMethodChannel - .invokeMethod('connect', {'name': deviceName}); + return await _eSenseManagerMethodChannel.invokeMethod( + 'connect', {'name': deviceName}) ?? + false; } /// Disconnects the device (if connected). @@ -82,9 +84,15 @@ class ESenseManager { /// after the disconnection has taken place. /// Returns `true` if the disconnection was successfully made, `false` /// otherwise. - Future disconnect() async => (connected) - ? await _eSenseManagerMethodChannel.invokeMethod('disconnect') - : false; + Future disconnect() async { + _eventStream = null; + _sensorStream = null; + + return (connected) + ? await _eSenseManagerMethodChannel.invokeMethod('disconnect') ?? + false + : false; + } /// Checks the BTLE connection if the device is connected or not. /// @@ -108,7 +116,7 @@ class ESenseManager { _samplingRate = rate; // for some strange reason, iOS does not accept an int as argument // hence, [rate] is converted to a string - return await _eSenseManagerMethodChannel.invokeMethod( + return await _eSenseManagerMethodChannel.invokeMethod( 'setSamplingRate', {'rate': '$rate'}) ?? false; } @@ -118,9 +126,11 @@ class ESenseManager { /// The event [DeviceNameRead] is fired when the name has been read. /// Returns `true` if the request was successfully made, `false` otherwise. Future getDeviceName() async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - return await _eSenseManagerMethodChannel.invokeMethod('getDeviceName') ?? + } + return await _eSenseManagerMethodChannel + .invokeMethod('getDeviceName') ?? false; } @@ -130,9 +140,10 @@ class ESenseManager { /// Returns `true` if the request was successfully made, `false` otherwise. Future setDeviceName(String deviceName) async { assert(deviceName.isNotEmpty && deviceName.length < 22, - 'The device name must be more that zero and less than 22 characteres long.'); - if (!connected) + 'The device name must be more that zero and less than 22 characters long.'); + if (!connected) { throw ESenseException('Not connected to any eSense device.'); + } return await _eSenseManagerMethodChannel.invokeMethod( 'setDeviceName', {'deviceName': deviceName}) ?? false; @@ -143,10 +154,11 @@ class ESenseManager { /// The event [BatteryRead] is fired when the voltage has been read. /// Returns `true` if the request was successfully made, `false` otherwise. Future getBatteryVoltage() async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); + } return await _eSenseManagerMethodChannel - .invokeMethod('getBatteryVoltage') ?? + .invokeMethod('getBatteryVoltage') ?? false; } @@ -156,10 +168,11 @@ class ESenseManager { /// The event [AccelerometerOffsetRead] is fired when the offset has been read. /// Returns `true` if the request was successfully made, `false` otherwise. Future getAccelerometerOffset() async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); + } return await _eSenseManagerMethodChannel - .invokeMethod('getAccelerometerOffset') ?? + .invokeMethod('getAccelerometerOffset') ?? false; } @@ -170,10 +183,11 @@ class ESenseManager { /// parameter values has been read. /// Returns `true` if the request was successfully made, `false` otherwise. Future getAdvertisementAndConnectionInterval() async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); + } return await _eSenseManagerMethodChannel - .invokeMethod('getAdvertisementAndConnectionInterval') ?? + .invokeMethod('getAdvertisementAndConnectionInterval') ?? false; } @@ -193,11 +207,12 @@ class ESenseManager { /// greater than or equal to 20. /// /// Returns `true` if the request was successfully made, `false` otherwise. - Future setAdvertisementAndConnectiontInterval(int advMinInterval, + Future setAdvertisementAndConnectionInterval(int advMinInterval, int advMaxInterval, int connMinInterval, int connMaxInterval) async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - return await _eSenseManagerMethodChannel.invokeMethod( + } + return await _eSenseManagerMethodChannel.invokeMethod( 'setAdvertisementAndConnectiontInterval', { 'advMinInterval': advMinInterval, 'advMaxInterval': advMaxInterval, @@ -214,9 +229,11 @@ class ESenseManager { /// The event [SensorConfigRead] is fired when the offset has been read. /// Returns `true` if the request was successfully made, `false` otherwise. Future getSensorConfig() async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - return await _eSenseManagerMethodChannel.invokeMethod('getSensorConfig') ?? + } + return await _eSenseManagerMethodChannel + .invokeMethod('getSensorConfig') ?? false; } @@ -224,9 +241,10 @@ class ESenseManager { /// /// Returns `true` if the request was successfully made, `false` otherwise. Future setSensorConfig(ESenseConfig config) async { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - return await _eSenseManagerMethodChannel.invokeMethod( + } + return await _eSenseManagerMethodChannel.invokeMethod( 'setSensorConfig', config.toMap()) ?? false; } @@ -248,10 +266,12 @@ class ESenseManager { if (_connectionEventStream == null) { _connectionEventStream = _eSenseConnectionEventChannel .receiveBroadcastStream() - .map((type) => ConnectionEvent.fromString(type)); + .map((type) => ConnectionEvent.fromString('$type')); // listen to the connection event in order to set the [connection] status - _connectionEventStream!.listen((ConnectionEvent event) { + _connectionEventStream?.listen((ConnectionEvent event) { + print('$runtimeType - event: $event'); + switch (event.type) { case ConnectionType.connected: connected = true; @@ -273,14 +293,12 @@ class ESenseManager { /// Throws an [ESenseException] if not connected to an eSense device. /// Wait until [connected] before using this stream. Stream get eSenseEvents { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - if (_eventStream == null) { - _eventStream = _eSenseEventChannel - .receiveBroadcastStream() - .map((event) => ESenseEvent.fromMap(event)); } - return _eventStream!; + + return _eventStream ??= _eSenseEventChannel.receiveBroadcastStream().map( + (event) => event is Map ? ESenseEvent.fromMap(event) : ESenseEvent()); } /// Get the stream of sensor events. @@ -291,14 +309,14 @@ class ESenseManager { /// Throws an [ESenseException] if not connected to an eSense device. /// Wait until [connected] before using this stream. Stream get sensorEvents { - if (!connected) + if (!connected) { throw ESenseException('Not connected to any eSense device.'); - if (_sensorStream == null) { - _sensorStream = _eSenseSensorEventChannel - .receiveBroadcastStream() - .map((event) => SensorEvent.fromMap(event)); } - return _sensorStream!; + + return _sensorStream ??= _eSenseSensorEventChannel + .receiveBroadcastStream() + .map((event) => + event is Map ? SensorEvent.fromMap(event) : SensorEvent.empty()); } } @@ -306,5 +324,6 @@ class ESenseManager { class ESenseException implements Exception { final String message; ESenseException(this.message); + @override String toString() => '$runtimeType - $message'; } diff --git a/packages/esense_flutter/lib/esense_events.dart b/packages/esense_flutter/lib/esense_events.dart index 77df95b56..b7c0c9627 100644 --- a/packages/esense_flutter/lib/esense_events.dart +++ b/packages/esense_flutter/lib/esense_events.dart @@ -69,12 +69,23 @@ class SensorEvent { this.gyro, }); + factory SensorEvent.empty() => + SensorEvent(timestamp: DateTime.now(), packetIndex: -1); + factory SensorEvent.fromMap(Map map) { - //map.forEach((key, value) => print(' > map[$key] = $value')); - DateTime time = DateTime.fromMillisecondsSinceEpoch(map['timestamp']); - int index = map['packetIndex'] ?? -1; - List accl = [map['accel.x'], map['accel.y'], map['accel.z']]; - List gyro = [map['gyro.x'], map['gyro.y'], map['gyro.z']]; + DateTime time = + DateTime.fromMillisecondsSinceEpoch(map['timestamp'] as int); + int index = map['packetIndex'] as int? ?? -1; + List accl = [ + map['accel.x'] as int, + map['accel.y'] as int, + map['accel.z'] as int + ]; + List gyro = [ + map['gyro.x'] as int, + map['gyro.y'] as int, + map['gyro.z'] as int + ]; return SensorEvent( timestamp: time, @@ -94,10 +105,10 @@ class ESenseEvent { ESenseEvent(); factory ESenseEvent.fromMap(Map map) { - final String type = map['type']; + final String type = map['type'] as String; switch (type) { case 'Listen': - return RegisterListenerEvent(map['success']); + return RegisterListenerEvent(map['success'] as bool); case 'DeviceNameRead': return DeviceNameRead.fromMap(map); case 'BatteryRead': @@ -142,7 +153,8 @@ class AccelerometerOffsetRead extends ESenseEvent { AccelerometerOffsetRead(this.offsetX, this.offsetY, this.offsetZ) : super(); factory AccelerometerOffsetRead.fromMap(Map map) => - AccelerometerOffsetRead(map['offsetX'], map['offsetY'], map['offsetZ']); + AccelerometerOffsetRead( + map['offsetX'] as int, map['offsetY'] as int, map['offsetZ'] as int); @override String toString() => '$runtimeType - ' @@ -175,10 +187,10 @@ class AdvertisementAndConnectionIntervalRead extends ESenseEvent { factory AdvertisementAndConnectionIntervalRead.fromMap( Map map) => AdvertisementAndConnectionIntervalRead( - map['minAdvertisementInterval'], - map['maxAdvertisementInterval'], - map['minConnectionInterval'], - map['maxConnectionInterval'], + map['minAdvertisementInterval'] as int?, + map['maxAdvertisementInterval'] as int?, + map['minConnectionInterval'] as int?, + map['maxConnectionInterval'] as int?, ); @override @@ -196,7 +208,7 @@ class BatteryRead extends ESenseEvent { BatteryRead(this.voltage) : super(); factory BatteryRead.fromMap(Map map) => - BatteryRead(map['voltage']); + BatteryRead(map['voltage'] as double?); @override String toString() => '$runtimeType - voltage: $voltage'; @@ -209,7 +221,7 @@ class ButtonEventChanged extends ESenseEvent { ButtonEventChanged(this.pressed) : super(); factory ButtonEventChanged.fromMap(Map map) => - ButtonEventChanged(map['pressed']); + ButtonEventChanged(map['pressed'] as bool); @override String toString() => '$runtimeType - pressed: $pressed'; @@ -222,7 +234,7 @@ class DeviceNameRead extends ESenseEvent { DeviceNameRead(this.deviceName) : super(); factory DeviceNameRead.fromMap(Map map) => - DeviceNameRead(map['deviceName']); + DeviceNameRead(map['deviceName'] as String?); @override String toString() => '$runtimeType - name: $deviceName'; diff --git a/packages/esense_flutter/pubspec.yaml b/packages/esense_flutter/pubspec.yaml index 94de1f9dc..cffdb38e6 100644 --- a/packages/esense_flutter/pubspec.yaml +++ b/packages/esense_flutter/pubspec.yaml @@ -1,11 +1,11 @@ name: esense_flutter description: The eSense Flutter Plugin supporting the eSense earable computing devices from Nokia Bell Labs, Cambridge. -version: 0.6.2 +version: 1.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/esense_flutter environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.12.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -14,6 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: any # The following section is specific to Flutter. flutter: diff --git a/packages/flutter_foreground_service/CHANGELOG.md b/packages/flutter_foreground_service/CHANGELOG.md index 367c96ee3..9ab1feef6 100644 --- a/packages/flutter_foreground_service/CHANGELOG.md +++ b/packages/flutter_foreground_service/CHANGELOG.md @@ -1,20 +1,35 @@ +## 0.4.1 + +- Lowered minSdkVersion to 23 + +## 0.4.0 + +- Upgraded Kotlin and AGP +- Upgraded sdk version for Android + ## 0.3.0 -* Support for null-safety. + +- Support for null-safety. ## 0.2.1 -* Rebuild project from scratch due to iOS issues with old generated code. + +- Rebuild project from scratch due to iOS issues with old generated code. ## 0.2.0 -* No longer produces an exception when invoked on iOS. - + +- No longer produces an exception when invoked on iOS. + ## 0.1.1 -* Fixed expected launcher icon path -* Previously `drawable` was the expected folder -* Now the `mipmap` folder is expected + +- Fixed expected launcher icon path +- Previously `drawable` was the expected folder +- Now the `mipmap` folder is expected ## 0.1.0+1 -* Updated documentation + +- Updated documentation ## 0.1.0 -* Forked from https://pub.dev/packages/foreground_service -* Changed hardcoded notification icon name to 'ic_launcher.png' \ No newline at end of file + +- Forked from https://pub.dev/packages/foreground_service +- Changed hardcoded notification icon name to 'ic_launcher.png' diff --git a/packages/flutter_foreground_service/android/build.gradle b/packages/flutter_foreground_service/android/build.gradle index ec36b50e8..f1340a1f7 100644 --- a/packages/flutter_foreground_service/android/build.gradle +++ b/packages/flutter_foreground_service/android/build.gradle @@ -2,14 +2,14 @@ group 'dk.cachet.flutter_foreground_service' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 23 } lintOptions { disable 'InvalidPackage' diff --git a/packages/flutter_foreground_service/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_foreground_service/android/gradle/wrapper/gradle-wrapper.properties index 01a286e96..c1ac8e432 100644 --- a/packages/flutter_foreground_service/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_foreground_service/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/flutter_foreground_service/android/src/main/kotlin/dk/cachet/flutter_foreground_service/FlutterForegroundServicePlugin.kt b/packages/flutter_foreground_service/android/src/main/kotlin/dk/cachet/flutter_foreground_service/FlutterForegroundServicePlugin.kt index 6c47cd947..bf6367145 100644 --- a/packages/flutter_foreground_service/android/src/main/kotlin/dk/cachet/flutter_foreground_service/FlutterForegroundServicePlugin.kt +++ b/packages/flutter_foreground_service/android/src/main/kotlin/dk/cachet/flutter_foreground_service/FlutterForegroundServicePlugin.kt @@ -501,7 +501,7 @@ class FlutterForegroundServicePlugin: FlutterPlugin, MethodCallHandler, IntentSe class NotificationHelper(val notificationId: Int = 1){ //things that MUST be set for a notification to function property (probably) - + //setContentTitle //setContentText //setSmallIcon @@ -774,4 +774,4 @@ enum class AndroidNotifiationPriority{ } ) } -} \ No newline at end of file +} diff --git a/packages/flutter_foreground_service/example/android/app/build.gradle b/packages/flutter_foreground_service/example/android/app/build.gradle index 605c8affe..520f6c269 100644 --- a/packages/flutter_foreground_service/example/android/app/build.gradle +++ b/packages/flutter_foreground_service/example/android/app/build.gradle @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 33 + sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +40,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dk.cachet.flutter_foreground_service_example" - minSdkVersion 16 - targetSdkVersion 29 + minSdkVersion 23 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/flutter_foreground_service/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_foreground_service/example/android/app/src/main/AndroidManifest.xml index 843c6b6a8..685745cac 100644 --- a/packages/flutter_foreground_service/example/android/app/src/main/AndroidManifest.xml +++ b/packages/flutter_foreground_service/example/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + diff --git a/packages/health/example/android/build.gradle b/packages/health/example/android/build.gradle index 2d972b333..e306d8ebf 100644 --- a/packages/health/example/android/build.gradle +++ b/packages/health/example/android/build.gradle @@ -1,8 +1,8 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.8.0' repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 106bd234e..44193fc45 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -222,10 +222,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -256,6 +258,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/packages/health/example/ios/Runner/Info.plist b/packages/health/example/ios/Runner/Info.plist index 38e094a95..b4af43739 100644 --- a/packages/health/example/ios/Runner/Info.plist +++ b/packages/health/example/ios/Runner/Info.plist @@ -47,5 +47,7 @@ UIViewControllerBasedStatusBarAppearance + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index ca50a6b05..7e570a2fd 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:health/health.dart'; +import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; void main() => runApp(HealthApp()); @@ -17,6 +17,7 @@ enum AppState { FETCHING_DATA, DATA_READY, NO_DATA, + AUTHORIZED, AUTH_NOT_GRANTED, DATA_ADDED, DATA_DELETED, @@ -28,51 +29,36 @@ enum AppState { class _HealthAppState extends State { List _healthDataList = []; AppState _state = AppState.DATA_NOT_FETCHED; - int _nofSteps = 10; - double _mgdl = 10.0; + int _nofSteps = 0; + + // Define the types to get. + // 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] + // Add more - like AUDIOGRAM, HEADACHE_SEVERE etc. to try them. + static final types = dataTypesAndroid; + // Or selected types + // static final types = [ + // HealthDataType.WEIGHT, + // HealthDataType.STEPS, + // HealthDataType.HEIGHT, + // HealthDataType.BLOOD_GLUCOSE, + // HealthDataType.WORKOUT, + // HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + // HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + // // Uncomment these lines on iOS - only available on iOS + // // HealthDataType.AUDIOGRAM + // ]; + + // with coresponsing permissions + // READ only + // final permissions = types.map((e) => HealthDataAccess.READ).toList(); + // Or READ and WRITE + final permissions = types.map((e) => HealthDataAccess.READ_WRITE).toList(); // create a HealthFactory for use in the app - HealthFactory health = HealthFactory(); - - /// Fetch data points from the health plugin and show them in the app. - Future fetchData() async { - setState(() => _state = AppState.FETCHING_DATA); - - // define the types to get - final types = [ - HealthDataType.STEPS, - HealthDataType.WEIGHT, - HealthDataType.HEIGHT, - HealthDataType.BLOOD_GLUCOSE, - HealthDataType.WORKOUT, - HealthDataType.BLOOD_PRESSURE_DIASTOLIC, - HealthDataType.BLOOD_PRESSURE_SYSTOLIC, - // Uncomment these lines on iOS - only available on iOS - // HealthDataType.AUDIOGRAM - ]; - - // with coresponsing permissions - final permissions = [ - HealthDataAccess.READ, - HealthDataAccess.READ, - HealthDataAccess.READ, - HealthDataAccess.READ, - HealthDataAccess.READ, - HealthDataAccess.READ, - HealthDataAccess.READ, - // HealthDataAccess.READ, - ]; - - // get data within the last 24 hours - final now = DateTime.now(); - final yesterday = now.subtract(Duration(hours: 24)); - // requesting access to the data types before reading them - // note that strictly speaking, the [permissions] are not - // needed, since we only want READ access. - bool requested = - await health.requestAuthorization(types, permissions: permissions); - print('requested: $requested'); + HealthFactory health = HealthFactory(useHealthConnectIfAvailable: true); + Future authorize() async { // If we are trying to read Step Count, Workout, Sleep or other data that requires // the ACTIVITY_RECOGNITION permission, we need to request the permission first. // This requires a special request authorization call. @@ -81,37 +67,61 @@ class _HealthAppState extends State { await Permission.activityRecognition.request(); await Permission.location.request(); - // Clear old data points - _healthDataList.clear(); + // Check if we have permission + bool? hasPermissions = + await health.hasPermissions(types, permissions: permissions); - if (requested) { + // hasPermissions = false because the hasPermission cannot disclose if WRITE access exists. + // Hence, we have to request with WRITE as well. + hasPermissions = false; + + bool authorized = false; + if (!hasPermissions) { + // requesting access to the data types before reading them try { - // fetch health data - List healthData = - await health.getHealthDataFromTypes(yesterday, now, types); - // save all the new data points (only the first 100) - _healthDataList.addAll((healthData.length < 100) - ? healthData - : healthData.sublist(0, 100)); + authorized = + await health.requestAuthorization(types, permissions: permissions); } catch (error) { - print("Exception in getHealthDataFromTypes: $error"); + print("Exception in authorize: $error"); } + } + + setState(() => _state = + (authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED); + } - // filter out duplicates - _healthDataList = HealthFactory.removeDuplicates(_healthDataList); + /// Fetch data points from the health plugin and show them in the app. + Future fetchData() async { + setState(() => _state = AppState.FETCHING_DATA); - // print the results - _healthDataList.forEach((x) => print(x)); + // get data within the last 24 hours + final now = DateTime.now(); + final yesterday = now.subtract(Duration(hours: 24)); - // update the UI to display the results - setState(() { - _state = - _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY; - }); - } else { - print("Authorization not granted"); - setState(() => _state = AppState.DATA_NOT_FETCHED); + // Clear old data points + _healthDataList.clear(); + + try { + // fetch health data + List healthData = + await health.getHealthDataFromTypes(yesterday, now, types); + // save all the new data points (only the first 100) + _healthDataList.addAll( + (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); + } catch (error) { + print("Exception in getHealthDataFromTypes: $error"); } + + // filter out duplicates + _healthDataList = HealthFactory.removeDuplicates(_healthDataList); + + // print the results + _healthDataList.forEach((x) => print(x)); + + // update the UI to display the results + setState(() { + _state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY; + }); } /// Add some random health data. @@ -119,70 +129,45 @@ class _HealthAppState extends State { final now = DateTime.now(); final earlier = now.subtract(Duration(minutes: 20)); - final types = [ - HealthDataType.STEPS, - HealthDataType.HEIGHT, - HealthDataType.BLOOD_GLUCOSE, - HealthDataType.WORKOUT, // Requires Google Fit on Android - HealthDataType.BLOOD_PRESSURE_DIASTOLIC, - HealthDataType.BLOOD_PRESSURE_SYSTOLIC, - // Uncomment these lines on iOS - only available on iOS - // HealthDataType.AUDIOGRAM, - ]; - final rights = [ - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - // HealthDataAccess.WRITE - ]; - final permissions = [ - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - // HealthDataAccess.READ_WRITE, - ]; - bool? hasPermissions = - await HealthFactory.hasPermissions(types, permissions: rights); - if (hasPermissions == false) { - await health.requestAuthorization(types, permissions: permissions); - } - - // Store a count of steps taken - _nofSteps = Random().nextInt(10); - bool success = await health.writeHealthData( - _nofSteps.toDouble(), HealthDataType.STEPS, earlier, now); - - // Store a height + // 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] + // Add more - like AUDIOGRAM, HEADACHE_SEVERE etc. to try them. + bool success = true; + success &= await health.writeHealthData( + 1.925, HealthDataType.HEIGHT, earlier, now); success &= - await health.writeHealthData(1.93, HealthDataType.HEIGHT, earlier, now); - - // Store a Blood Glucose measurement - _mgdl = Random().nextInt(10) * 1.0; + await health.writeHealthData(90, HealthDataType.WEIGHT, earlier, now); success &= await health.writeHealthData( - _mgdl, HealthDataType.BLOOD_GLUCOSE, now, now); - - // Store a workout eg. running + 90, HealthDataType.HEART_RATE, earlier, now); + success &= + await health.writeHealthData(90, HealthDataType.STEPS, earlier, now); + success &= await health.writeHealthData( + 200, HealthDataType.ACTIVE_ENERGY_BURNED, earlier, now); + success &= await health.writeHealthData( + 70, HealthDataType.HEART_RATE, earlier, now); + success &= await health.writeHealthData( + 37, HealthDataType.BODY_TEMPERATURE, earlier, now); + success &= await health.writeBloodOxygen(98, earlier, now, flowRate: 1.0); + success &= await health.writeHealthData( + 105, HealthDataType.BLOOD_GLUCOSE, earlier, now); + success &= + await health.writeHealthData(1.8, HealthDataType.WATER, earlier, now); success &= await health.writeWorkoutData( - HealthWorkoutActivityType.RUNNING, - earlier, - now, - // The following are optional parameters - // and the UNITS are functional on iOS ONLY! - totalEnergyBurned: 230, - totalEnergyBurnedUnit: HealthDataUnit.KILOCALORIE, - totalDistance: 1234, - totalDistanceUnit: HealthDataUnit.FOOT, - ); - - success &= await health.writeBloodPressure(120, 90, now, now); + HealthWorkoutActivityType.AMERICAN_FOOTBALL, + now.subtract(Duration(minutes: 15)), + now, + totalDistance: 2430, + totalEnergyBurned: 400); + success &= await health.writeBloodPressure(90, 80, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_REM, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_ASLEEP, earlier, now); success &= await health.writeHealthData( - 3, HealthDataType.SLEEP_ASLEEP, now.subtract(Duration(hours: 3)), now); + 0.0, HealthDataType.SLEEP_AWAKE, earlier, now); + success &= await health.writeHealthData( + 0.0, HealthDataType.SLEEP_DEEP, earlier, now); // Store an Audiogram // Uncomment these on iOS - only available on iOS @@ -210,49 +195,12 @@ class _HealthAppState extends State { /// Delete some random health data. Future deleteData() async { final now = DateTime.now(); - final earlier = now.subtract(Duration(minutes: 30)); - - final types = [ - HealthDataType.STEPS, - HealthDataType.HEIGHT, - HealthDataType.BLOOD_GLUCOSE, - HealthDataType.WORKOUT, // Requires Google Fit on Android - HealthDataType.BLOOD_PRESSURE_SYSTOLIC, - // Uncomment these lines on iOS - only available on iOS - // HealthDataType.AUDIOGRAM, - ]; - final rights = [ - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - HealthDataAccess.WRITE, - // HealthDataAccess.WRITE - ]; - final permissions = [ - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - HealthDataAccess.READ_WRITE, - // HealthDataAccess.READ_WRITE, - ]; - bool? hasPermissions = - await HealthFactory.hasPermissions(types, permissions: rights); - if (hasPermissions == false) { - await health.requestAuthorization(types, permissions: permissions); - } - - bool success = false; + final earlier = now.subtract(Duration(hours: 24)); - success = await health.delete(HealthDataType.STEPS, earlier, now); - success &= await health.delete(HealthDataType.HEIGHT, earlier, now); - success &= await health.delete(HealthDataType.BLOOD_GLUCOSE, earlier, now); - success &= await health.delete(HealthDataType.WORKOUT, earlier, now); - success &= await health.delete( - HealthDataType.BLOOD_PRESSURE_SYSTOLIC, - earlier, - now); // on Android this deletes both systolic and diastolic measurements. + bool success = true; + for (HealthDataType type in types) { + success &= await health.delete(type, earlier, now); + } setState(() { _state = success ? AppState.DATA_DELETED : AppState.DATA_NOT_DELETED; @@ -288,6 +236,14 @@ class _HealthAppState extends State { } } + Future revokeAccess() async { + try { + await health.revokePermissions(); + } catch (error) { + print("Caught exception in revokeAccess: $error"); + } + } + Widget _contentFetchingData() { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -346,6 +302,10 @@ class _HealthAppState extends State { ); } + Widget _authorized() { + return Text('Authorization granted!'); + } + Widget _authorizationNotGranted() { return Text('Authorization not given. ' 'For Android please check your OAUTH2 client ID is correct in Google Developer Console. ' @@ -379,6 +339,8 @@ class _HealthAppState extends State { return _contentNoData(); else if (_state == AppState.FETCHING_DATA) return _contentFetchingData(); + else if (_state == AppState.AUTHORIZED) + return _authorized(); else if (_state == AppState.AUTH_NOT_GRANTED) return _authorizationNotGranted(); else if (_state == AppState.DATA_ADDED) @@ -399,38 +361,65 @@ class _HealthAppState extends State { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Health Example'), - actions: [ - IconButton( - icon: Icon(Icons.file_download), - onPressed: () { - fetchData(); - }, - ), - IconButton( - onPressed: () { - addData(); - }, - icon: Icon(Icons.add), - ), - IconButton( - onPressed: () { - deleteData(); - }, - icon: Icon(Icons.delete), + appBar: AppBar( + title: const Text('Health Example'), + ), + body: Container( + child: Column( + children: [ + Wrap( + spacing: 10, + children: [ + TextButton( + onPressed: authorize, + child: + Text("Auth", style: TextStyle(color: Colors.white)), + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue))), + TextButton( + onPressed: fetchData, + child: Text("Fetch Data", + style: TextStyle(color: Colors.white)), + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.blue))), + 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))), + ], ), - IconButton( - onPressed: () { - fetchStepData(); - }, - icon: Icon(Icons.nordic_walking), - ) + Divider(thickness: 3), + Expanded(child: Center(child: _content())) ], ), - body: Center( - child: _content(), - )), + ), + ), ); } } diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart new file mode 100644 index 000000000..e2756e09e --- /dev/null +++ b/packages/health/example/lib/util.dart @@ -0,0 +1,81 @@ +import 'package:health/health.dart'; + +/// List of data types available on iOS +const List dataTypesIOS = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.AUDIOGRAM, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.BODY_MASS_INDEX, + HealthDataType.BODY_TEMPERATURE, + HealthDataType.DIETARY_CARBS_CONSUMED, + HealthDataType.DIETARY_ENERGY_CONSUMED, + HealthDataType.DIETARY_FATS_CONSUMED, + HealthDataType.DIETARY_PROTEIN_CONSUMED, + HealthDataType.ELECTRODERMAL_ACTIVITY, + HealthDataType.FORCED_EXPIRATORY_VOLUME, + HealthDataType.HEART_RATE, + HealthDataType.HEART_RATE_VARIABILITY_SDNN, + HealthDataType.HEIGHT, + HealthDataType.HIGH_HEART_RATE_EVENT, + HealthDataType.RESPIRATORY_RATE, + HealthDataType.PERIPHERAL_PERFUSION_INDEX, + HealthDataType.IRREGULAR_HEART_RATE_EVENT, + HealthDataType.LOW_HEART_RATE_EVENT, + HealthDataType.RESTING_HEART_RATE, + HealthDataType.STEPS, + HealthDataType.WAIST_CIRCUMFERENCE, + HealthDataType.WALKING_HEART_RATE, + HealthDataType.WEIGHT, + HealthDataType.FLIGHTS_CLIMBED, + HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.MINDFULNESS, + HealthDataType.SLEEP_AWAKE, + HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_REM, + HealthDataType.WATER, + HealthDataType.EXERCISE_TIME, + HealthDataType.WORKOUT, + HealthDataType.HEADACHE_NOT_PRESENT, + HealthDataType.HEADACHE_MILD, + HealthDataType.HEADACHE_MODERATE, + HealthDataType.HEADACHE_SEVERE, + HealthDataType.HEADACHE_UNSPECIFIED, + //HealthDataType.ELECTROCARDIOGRAM, +]; + +/// List of data types available on Android +const List dataTypesAndroid = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.BLOOD_GLUCOSE, + HealthDataType.BLOOD_OXYGEN, + HealthDataType.BLOOD_PRESSURE_DIASTOLIC, + HealthDataType.BLOOD_PRESSURE_SYSTOLIC, + HealthDataType.BODY_FAT_PERCENTAGE, + HealthDataType.HEIGHT, + HealthDataType.WEIGHT, + // HealthDataType.BODY_MASS_INDEX, + HealthDataType.BODY_TEMPERATURE, + HealthDataType.HEART_RATE, + 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_DEEP, + HealthDataType.SLEEP_REM, + HealthDataType.SLEEP_SESSION, + HealthDataType.WATER, + HealthDataType.WORKOUT, + HealthDataType.RESTING_HEART_RATE, + HealthDataType.FLIGHTS_CLIMBED, +]; diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index aae5c28e3..da07ca12d 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1,196 +1,202 @@ import Flutter -import UIKit import HealthKit +import UIKit public class SwiftHealthPlugin: NSObject, FlutterPlugin { - - let healthStore = HKHealthStore() - var healthDataTypes = [HKSampleType]() - var heartRateEventTypes = Set() - var headacheType = Set() - var allDataTypes = Set() - var dataTypesDict: [String: HKSampleType] = [:] - var unitDict: [String: HKUnit] = [:] - var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] - - // Health Data Type Keys - let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" - let AUDIOGRAM = "AUDIOGRAM" - let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" - let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" - let BLOOD_OXYGEN = "BLOOD_OXYGEN" - let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" - let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" - let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" - let BODY_MASS_INDEX = "BODY_MASS_INDEX" - let BODY_TEMPERATURE = "BODY_TEMPERATURE" - 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 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 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" - let RESTING_HEART_RATE = "RESTING_HEART_RATE" - let STEPS = "STEPS" - let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE" - let WALKING_HEART_RATE = "WALKING_HEART_RATE" - let WEIGHT = "WEIGHT" - let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING" - 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_AWAKE = "SLEEP_AWAKE" - let EXERCISE_TIME = "EXERCISE_TIME" - let WORKOUT = "WORKOUT" - let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" - let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT" - let HEADACHE_MILD = "HEADACHE_MILD" - let HEADACHE_MODERATE = "HEADACHE_MODERATE" - let HEADACHE_SEVERE = "HEADACHE_SEVERE" - let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" + + let healthStore = HKHealthStore() + var healthDataTypes = [HKSampleType]() + var heartRateEventTypes = Set() + var headacheType = Set() + var allDataTypes = Set() + var dataTypesDict: [String: HKSampleType] = [:] + var unitDict: [String: HKUnit] = [:] + var workoutActivityTypeMap: [String: HKWorkoutActivityType] = [:] + + // Health Data Type Keys + let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" + let AUDIOGRAM = "AUDIOGRAM" + let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" + let BLOOD_GLUCOSE = "BLOOD_GLUCOSE" + let BLOOD_OXYGEN = "BLOOD_OXYGEN" + let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" + let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" + let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" + let BODY_MASS_INDEX = "BODY_MASS_INDEX" + let BODY_TEMPERATURE = "BODY_TEMPERATURE" + 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 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 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" + let RESTING_HEART_RATE = "RESTING_HEART_RATE" + let RESPIRATORY_RATE = "RESPIRATORY_RATE" + let PERIPHERAL_PERFUSION_INDEX = "PERIPHERAL_PERFUSION_INDEX" + let STEPS = "STEPS" + let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE" + let WALKING_HEART_RATE = "WALKING_HEART_RATE" + let WEIGHT = "WEIGHT" + let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING" + 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_AWAKE = "SLEEP_AWAKE" + let SLEEP_DEEP = "SLEEP_DEEP" + let SLEEP_REM = "SLEEP_REM" + + let EXERCISE_TIME = "EXERCISE_TIME" + let WORKOUT = "WORKOUT" + let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED" + let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT" + let HEADACHE_MILD = "HEADACHE_MILD" + let HEADACHE_MODERATE = "HEADACHE_MODERATE" + let HEADACHE_SEVERE = "HEADACHE_SEVERE" + let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM" let NUTRITION = "NUTRITION" - - // 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 - let GRAM = "GRAM" - let KILOGRAM = "KILOGRAM" - let OUNCE = "OUNCE" - let POUND = "POUND" - let STONE = "STONE" - let METER = "METER" - let INCH = "INCH" - let FOOT = "FOOT" - let YARD = "YARD" - let MILE = "MILE" - let LITER = "LITER" - let MILLILITER = "MILLILITER" - let FLUID_OUNCE_US = "FLUID_OUNCE_US" - let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL" - let CUP_US = "CUP_US" - let CUP_IMPERIAL = "CUP_IMPERIAL" - let PINT_US = "PINT_US" - let PINT_IMPERIAL = "PINT_IMPERIAL" - let PASCAL = "PASCAL" - let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY" - let INCHES_OF_MERCURY = "INCHES_OF_MERCURY" - let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER" - let ATMOSPHERE = "ATMOSPHERE" - let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL" - let SECOND = "SECOND" - let MILLISECOND = "MILLISECOND" - let MINUTE = "MINUTE" - let HOUR = "HOUR" - let DAY = "DAY" - let JOULE = "JOULE" - let KILOCALORIE = "KILOCALORIE" - let LARGE_CALORIE = "LARGE_CALORIE" - let SMALL_CALORIE = "SMALL_CALORIE" - let DEGREE_CELSIUS = "DEGREE_CELSIUS" - let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT" - let KELVIN = "KELVIN" - let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL" - let HERTZ = "HERTZ" - let SIEMEN = "SIEMEN" - let VOLT = "VOLT" - let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT" - let COUNT = "COUNT" - let PERCENT = "PERCENT" - let BEATS_PER_MINUTE = "BEATS_PER_MINUTE" - let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER" - let UNKNOWN_UNIT = "UNKNOWN_UNIT" - let NO_UNIT = "NO_UNIT" - - struct PluginError: Error { - let message: String + + // 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 + let GRAM = "GRAM" + let KILOGRAM = "KILOGRAM" + let OUNCE = "OUNCE" + let POUND = "POUND" + let STONE = "STONE" + let METER = "METER" + let INCH = "INCH" + let FOOT = "FOOT" + let YARD = "YARD" + let MILE = "MILE" + let LITER = "LITER" + let MILLILITER = "MILLILITER" + let FLUID_OUNCE_US = "FLUID_OUNCE_US" + let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL" + let CUP_US = "CUP_US" + let CUP_IMPERIAL = "CUP_IMPERIAL" + let PINT_US = "PINT_US" + let PINT_IMPERIAL = "PINT_IMPERIAL" + let PASCAL = "PASCAL" + let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY" + let INCHES_OF_MERCURY = "INCHES_OF_MERCURY" + let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER" + let ATMOSPHERE = "ATMOSPHERE" + let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL" + let SECOND = "SECOND" + let MILLISECOND = "MILLISECOND" + let MINUTE = "MINUTE" + let HOUR = "HOUR" + let DAY = "DAY" + let JOULE = "JOULE" + let KILOCALORIE = "KILOCALORIE" + let LARGE_CALORIE = "LARGE_CALORIE" + let SMALL_CALORIE = "SMALL_CALORIE" + let DEGREE_CELSIUS = "DEGREE_CELSIUS" + let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT" + let KELVIN = "KELVIN" + let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL" + let HERTZ = "HERTZ" + let SIEMEN = "SIEMEN" + let VOLT = "VOLT" + let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT" + let COUNT = "COUNT" + let PERCENT = "PERCENT" + let BEATS_PER_MINUTE = "BEATS_PER_MINUTE" + let RESPIRATIONS_PER_MINUTE = "RESPIRATIONS_PER_MINUTE" + 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) + }/// Handle requestAuthorization + else if call.method.elementsEqual("requestAuthorization") { + try! requestAuthorization(call: call, result: result) } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_health", binaryMessenger: registrar.messenger()) - let instance = SwiftHealthPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) + + /// Handle getData + else if call.method.elementsEqual("getData") { + getData(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) } - - 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) - } - /// Handle requestAuthorization - 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 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 writeWorkoutData - else if (call.method.elementsEqual("writeWorkoutData")){ - try! writeWorkoutData(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) - } - + + /// Handle writeWorkoutData + else if call.method.elementsEqual("writeWorkoutData") { + try! writeWorkoutData(call: call, result: result) } - - func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { - result(HKHealthStore.isHealthDataAvailable()) + + /// 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) + } + + } + + 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], + var permissions = arguments?["permissions"] as? [Int], + types.count == permissions.count + else { + throw PluginError(message: "Invalid Arguments!") } - - func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - let arguments = call.arguments as? NSDictionary - guard var types = arguments?["types"] as? Array, - var permissions = arguments?["permissions"] as? Array, - types.count == permissions.count - else { - throw PluginError(message: "Invalid Arguments!") - } if let nutritionIndex = types.firstIndex(of: NUTRITION) { types.remove(at: nutritionIndex) @@ -206,51 +212,48 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { types.append(DIETARY_FATS_CONSUMED) permissions.append(nutritionPermission) } - - for (index, type) in types.enumerated() { - let sampleType = dataTypeLookUp(key: type) - let success = hasPermission(type: sampleType, access: permissions[index]) - if (success == nil || success == false) { - result(success) - return - } - } - - result(true) + + for (index, type) in types.enumerated() { + let sampleType = dataTypeLookUp(key: type) + let success = hasPermission(type: sampleType, access: permissions[index]) + if success == nil || success == false { + result(success) + return + } } - - - func hasPermission(type: HKSampleType, access: Int) -> Bool? { - - if #available(iOS 13.0, *) { - let status = healthStore.authorizationStatus(for: type) - switch access { - case 0: // READ - return nil - case 1: // WRITE - return (status == HKAuthorizationStatus.sharingAuthorized) - default: // READ_WRITE - return nil - } - } - else { - return nil - } + + result(true) + } + + func hasPermission(type: HKSampleType, access: Int) -> Bool? { + + if #available(iOS 13.0, *) { + let status = healthStore.authorizationStatus(for: type) + switch access { + case 0: // READ + return nil + case 1: // WRITE + return (status == HKAuthorizationStatus.sharingAuthorized) + default: // READ_WRITE + return nil + } + } else { + return nil } - - func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let types = arguments["types"] as? Array, - let permissions = arguments["permissions"] as? Array, - permissions.count == types.count - else { - throw PluginError(message: "Invalid Arguments!") - } - - - var typesToRead = Set() - var typesToWrite = Set() - for (index, key) in types.enumerated() { + } + + func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let types = arguments["types"] as? [String], + let permissions = arguments["permissions"] as? [Int], + permissions.count == types.count + else { + throw PluginError(message: "Invalid Arguments!") + } + + 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) @@ -262,138 +265,157 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { typesToWrite.insert(proteinType); typesToWrite.insert(fatType); } else { - let dataType = dataTypeLookUp(key: key) - let access = permissions[index] - switch access { - case 0: - typesToRead.insert(dataType) - case 1: - typesToWrite.insert(dataType) - default: - typesToRead.insert(dataType) - typesToWrite.insert(dataType) - } - } + let dataType = dataTypeLookUp(key: key) + let access = permissions[index] + switch access { + case 0: + typesToRead.insert(dataType) + case 1: + typesToWrite.insert(dataType) + default: + typesToRead.insert(dataType) + typesToWrite.insert(dataType) + } } - - if #available(iOS 13.0, *) { - healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in - DispatchQueue.main.async { - result(success) - } - } } - else { - result(false)// Handle the error here. + + if #available(iOS 13.0, *) { + healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { + (success, error) in + DispatchQueue.main.async { + result(success) } + } + } else { + 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) - else { - throw PluginError(message: "Invalid Arguments") - } - - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - - let sample: HKObject - - if (unitLookUp(key: type) == HKUnit.init(from: "")) { - sample = HKCategorySample(type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, end: dateTo) - } else { - let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) - - sample = HKQuantitySample(type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, end: dateTo) - } - - HKHealthStore().save(sample, withCompletion: { (success, error) in - if let err = error { - print("Error Saving \(type) Sample: \(err.localizedDescription)") - } - DispatchQueue.main.async { - result(success) - } - }) + } + + 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) + else { + throw PluginError(message: "Invalid Arguments") } - - func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let frequencies = (arguments["frequencies"] as? Array), - let leftEarSensitivities = (arguments["leftEarSensitivities"] as? Array), - let rightEarSensitivities = (arguments["rightEarSensitivities"] as? Array), - 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) - - var sensitivityPoints = [HKAudiogramSensitivityPoint]() - - for index in 0...frequencies.count-1 { - let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index]) - let dbUnit = HKUnit.decibelHearingLevel() - let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index]) - let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index]) - let sensitivityPoint = try HKAudiogramSensitivityPoint(frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right) - sensitivityPoints.append(sensitivityPoint) + + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + 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) + } else { + let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) + sample = HKQuantitySample( + type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, + end: dateTo) + } + + HKHealthStore().save( + sample, + withCompletion: { (success, error) in + if let err = error { + print("Error Saving \(type) Sample: \(err.localizedDescription)") } - - 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) + DispatchQueue.main.async { + result(success) } - - HKHealthStore().save(audiogram, withCompletion: { (success, error) in - if let err = error { - print("Error Saving Audiogram. Sample: \(err.localizedDescription)") - } - DispatchQueue.main.async { - result(success) - } - }) + }) + } + + func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let frequencies = (arguments["frequencies"] as? [Double]), + let leftEarSensitivities = (arguments["leftEarSensitivities"] as? [Double]), + let rightEarSensitivities = (arguments["rightEarSensitivities"] as? [Double]), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber) + else { + throw PluginError(message: "Invalid Arguments") } - 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) - 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() + let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index]) + let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index]) + let sensitivityPoint = try HKAudiogramSensitivityPoint( + 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 + if let err = error { + print("Error Saving Audiogram. Sample: \(err.localizedDescription)") } - let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) - let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + DispatchQueue.main.async { + result(success) + } + }) + } - let systolic_sample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), start: dateFrom, end: dateTo) - let diastolic_sample = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), start: dateFrom, end: dateTo) - - HKHealthStore().save([systolic_sample, diastolic_sample], withCompletion: { (success, error) in - if let err = error { - print("Error Saving Blood Pressure Sample: \(err.localizedDescription)") - } - DispatchQueue.main.async { - result(success) - } - }) + 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) + else { + throw PluginError(message: "Invalid Arguments") } + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let systolic_sample = HKQuantitySample( + type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, + quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), + start: dateFrom, end: dateTo) + let diastolic_sample = HKQuantitySample( + type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, + quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), + start: dateFrom, end: dateTo) + + HKHealthStore().save( + [systolic_sample, diastolic_sample], + withCompletion: { (success, error) in + if let err = error { + print("Error Saving Blood Pressure Sample: \(err.localizedDescription)") + } + DispatchQueue.main.async { + result(success) + } + }) + } func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws { guard let arguments = call.arguments as? NSDictionary, @@ -463,529 +485,577 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { } } - - func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { - guard let arguments = call.arguments as? NSDictionary, - let activityType = (arguments["activityType"] as? String), - let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber), - let ac = workoutActivityTypeMap[activityType] - else { - throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid") + func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws { + guard let arguments = call.arguments as? NSDictionary, + let activityType = (arguments["activityType"] as? String), + let startTime = (arguments["startTime"] as? NSNumber), + let endTime = (arguments["endTime"] as? NSNumber), + let ac = workoutActivityTypeMap[activityType] + 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( + unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb) + } + if let td = (arguments["totalDistance"] as? Double) { + 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 + if let err = error { + print("Error Saving Workout. Sample: \(err.localizedDescription)") } - - var totalEnergyBurned: HKQuantity? - var totalDistance: HKQuantity? = nil - - // Handle optional arguments - if let teb = (arguments["totalEnergyBurned"] as? Double) { - totalEnergyBurned = HKQuantity(unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb) + DispatchQueue.main.async { + result(success) } - if let td = (arguments["totalDistance"] as? Double) { - totalDistance = HKQuantity(unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td) + }) + } + + 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( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + + let deleteQuery = HKSampleQuery( + sampleType: dataType, predicate: predicate, 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 { + print("Error deleting \(dataType) Sample: \(err.localizedDescription)") } - - 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 - if let err = error { - print("Error Saving Workout. Sample: \(err.localizedDescription)") - } - DispatchQueue.main.async { - result(success) - } - }) + DispatchQueue.main.async { + result(success) + } + } } - - 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) + HKHealthStore().execute(deleteQuery) + } - let dataType = dataTypeLookUp(key: dataTypeKey) - - let predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: .strictStartDate) - let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let dataTypeKey = (arguments?["dataTypeKey"] as? String)! + let dataUnitKey = (arguments?["dataUnitKey"] as? String) + let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 + let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 + let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit - let deleteQuery = HKSampleQuery(sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [self] x, samplesOrNil, error in + // Convert dates from milliseconds to Date() + let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) + let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) - guard let samplesOrNil = samplesOrNil, error == nil else { - // Handle the error if necessary - print("Error deleting \(dataType)") - return - } + let dataType = dataTypeLookUp(key: dataTypeKey) + var unit: HKUnit? + if let dataUnitKey = dataUnitKey { + unit = unitDict[dataUnitKey] + } - // Delete the retrieved objects from the HealthKit store - HKHealthStore().delete(samplesOrNil) { (success, error) in - if let err = error { - print("Error deleting \(dataType) Sample: \(err.localizedDescription)") - } - DispatchQueue.main.async { - result(success) - } - } + let predicate = HKQuery.predicateForSamples( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + 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!), + "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) } - HKHealthStore().execute(deleteQuery) - } - - func getData(call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = call.arguments as? NSDictionary - let dataTypeKey = (arguments?["dataTypeKey"] as? String)! - let dataUnitKey = (arguments?["dataUnitKey"] as? String) - let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 - let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit - - // 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] + case var (samplesCategory as [HKCategorySample]) as Any: + + if dataTypeKey == self.SLEEP_IN_BED { + samplesCategory = samplesCategory.filter { $0.value == 0 } } - - let predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: .strictStartDate) - 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!), - "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) - } - - case var (samplesCategory as [HKCategorySample]) as Any: - if (dataTypeKey == self.SLEEP_IN_BED) { - samplesCategory = samplesCategory.filter { $0.value == 0 } - } - 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.HEADACHE_UNSPECIFIED) { - samplesCategory = samplesCategory.filter { $0.value == 0 } - } - if (dataTypeKey == self.HEADACHE_NOT_PRESENT) { - samplesCategory = samplesCategory.filter { $0.value == 1 } - } - if (dataTypeKey == self.HEADACHE_MILD) { - samplesCategory = samplesCategory.filter { $0.value == 2 } - } - if (dataTypeKey == self.HEADACHE_MODERATE) { - samplesCategory = samplesCategory.filter { $0.value == 3 } - } - if (dataTypeKey == self.HEADACHE_SEVERE) { - samplesCategory = samplesCategory.filter { $0.value == 4 } - } - let categories = samplesCategory.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "value": sample.value, - "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(categories) - } - - case let (samplesWorkout as [HKWorkout]) as Any: - - let dictionaries = samplesWorkout.map { sample -> NSDictionary in - return [ - "uuid": "\(sample.uuid)", - "workoutActivityType": workoutActivityTypeMap.first(where: {$0.value == sample.workoutActivityType})?.key, - "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), - "totalEnergyBurnedUnit": "KILOCALORIE", - "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), - "totalDistanceUnit": "METER", - "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) - } - - case let (samplesAudiogram as [HKAudiogramSample]) as Any: - let dictionaries = samplesAudiogram.map { sample -> NSDictionary in - var frequencies = [Double]() - var leftEarSensitivities = [Double]() - var rightEarSensitivities = [Double]() - for samplePoint in sample.sensitivityPoints { - frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) - leftEarSensitivities.append(samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) - rightEarSensitivities.append(samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) - } - return [ - "uuid": "\(sample.uuid)", - "frequencies": frequencies, - "leftEarSensitivities": leftEarSensitivities, - "rightEarSensitivities": rightEarSensitivities, - "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) - } - - default: - if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { - let dictionaries = ecgSamples.map(fetchEcgMeasurements) - DispatchQueue.main.async { - result(dictionaries) - } - } else { - DispatchQueue.main.async { - print("Error getting ECG - only available on iOS 14.0 and above!") - result(nil) - } - } - } + if dataTypeKey == self.SLEEP_AWAKE { + samplesCategory = samplesCategory.filter { $0.value == 2 } } - - HKHealthStore().execute(query) - } - - @available(iOS 14.0, *) - private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { - let semaphore = DispatchSemaphore(value: 0) - var voltageValues = [NSDictionary]() - let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in - switch (result) { - case let .measurement(measurement): - if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { - let voltage = voltageQuantity.doubleValue(for: HKUnit.volt()) - let timeSinceSampleStart = measurement.timeSinceSampleStart - voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart]) - } - case .done: - semaphore.signal() - case let .error(error): - print(error) - } - } - HKHealthStore().execute(voltageQuery) - semaphore.wait() - return [ + if dataTypeKey == self.SLEEP_ASLEEP { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.SLEEP_DEEP { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + if dataTypeKey == self.SLEEP_REM { + samplesCategory = samplesCategory.filter { $0.value == 5 } + } + if dataTypeKey == self.HEADACHE_UNSPECIFIED { + samplesCategory = samplesCategory.filter { $0.value == 0 } + } + if dataTypeKey == self.HEADACHE_NOT_PRESENT { + samplesCategory = samplesCategory.filter { $0.value == 1 } + } + if dataTypeKey == self.HEADACHE_MILD { + samplesCategory = samplesCategory.filter { $0.value == 2 } + } + if dataTypeKey == self.HEADACHE_MODERATE { + samplesCategory = samplesCategory.filter { $0.value == 3 } + } + if dataTypeKey == self.HEADACHE_SEVERE { + samplesCategory = samplesCategory.filter { $0.value == 4 } + } + let categories = samplesCategory.map { sample -> NSDictionary in + return [ "uuid": "\(sample.uuid)", - "voltageValues": voltageValues, - "averageHeartRate": sample.averageHeartRate?.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute())), - "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()), - "classification": sample.classification.rawValue, + "value": sample.value, "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 - ] - } - - 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 - - // 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(withStart: dateFrom, end: dateTo, options: .strictStartDate) - - let query = HKStatisticsQuery(quantityType: sampleType, - quantitySamplePredicate: predicate, - options: .cumulativeSum) { query, queryResult, error in - - guard let queryResult = queryResult else { - let error = error! as NSError - print("Error getting total steps in interval \(error.localizedDescription)") - - DispatchQueue.main.async { - result(nil) - } - return - } - - var steps = 0.0 - - if let quantity = queryResult.sumQuantity() { - let unit = HKUnit.count() - steps = quantity.doubleValue(for: unit) - } - - let totalSteps = Int(steps) - DispatchQueue.main.async { - result(totalSteps) - } + "source_name": sample.sourceRevision.source.name, + ] } - - HKHealthStore().execute(query) - } - - func unitLookUp(key: String) -> HKUnit { - guard let unit = unitDict[key] else { - return HKUnit.count() + DispatchQueue.main.async { + result(categories) } - return unit - } - - func dataTypeLookUp(key: String) -> HKSampleType { - guard let dataType_ = dataTypesDict[key] else { - return HKSampleType.quantityType(forIdentifier: .bodyMass)! + + case let (samplesWorkout as [HKWorkout]) as Any: + + let dictionaries = samplesWorkout.map { sample -> NSDictionary in + return [ + "uuid": "\(sample.uuid)", + "workoutActivityType": workoutActivityTypeMap.first(where: { + $0.value == sample.workoutActivityType + })?.key, + "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()), + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()), + "totalDistanceUnit": "METER", + "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, + ] } - return dataType_ - } - - func initializeTypes() { - // Initialize units - unitDict[GRAM] = HKUnit.gram() - unitDict[KILOGRAM] = HKUnit.gramUnit(with: .kilo) - unitDict[OUNCE] = HKUnit.ounce() - unitDict[POUND] = HKUnit.pound() - unitDict[STONE] = HKUnit.stone() - unitDict[METER] = HKUnit.meter() - unitDict[INCH] = HKUnit.inch() - unitDict[FOOT] = HKUnit.foot() - unitDict[YARD] = HKUnit.yard() - unitDict[MILE] = HKUnit.mile() - unitDict[LITER] = HKUnit.liter() - unitDict[MILLILITER] = HKUnit.literUnit(with: .milli) - unitDict[FLUID_OUNCE_US] = HKUnit.fluidOunceUS() - unitDict[FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial() - unitDict[CUP_US] = HKUnit.cupUS() - unitDict[CUP_IMPERIAL] = HKUnit.cupImperial() - unitDict[PINT_US] = HKUnit.pintUS() - unitDict[PINT_IMPERIAL] = HKUnit.pintImperial() - unitDict[PASCAL] = HKUnit.pascal() - unitDict[MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury() - unitDict[CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater() - unitDict[ATMOSPHERE] = HKUnit.atmosphere() - unitDict[DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel() - unitDict[SECOND] = HKUnit.second() - unitDict[MILLISECOND] = HKUnit.secondUnit(with: .milli) - unitDict[MINUTE] = HKUnit.minute() - unitDict[HOUR] = HKUnit.hour() - unitDict[DAY] = HKUnit.day() - unitDict[JOULE] = HKUnit.joule() - unitDict[KILOCALORIE] = HKUnit.kilocalorie() - unitDict[LARGE_CALORIE] = HKUnit.largeCalorie() - unitDict[SMALL_CALORIE] = HKUnit.smallCalorie() - unitDict[DEGREE_CELSIUS] = HKUnit.degreeCelsius() - unitDict[DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit() - unitDict[KELVIN] = HKUnit.kelvin() - unitDict[DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel() - unitDict[HERTZ] = HKUnit.hertz() - unitDict[SIEMEN] = HKUnit.siemen() - unitDict[INTERNATIONAL_UNIT] = HKUnit.internationalUnit() - unitDict[COUNT] = HKUnit.count() - unitDict[PERCENT] = HKUnit.percent() - unitDict[BEATS_PER_MINUTE] = HKUnit.init(from: "count/min") - 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 - workoutActivityTypeMap["FENCING"] = .fencing - workoutActivityTypeMap["GYMNASTICS"] = .gymnastics - workoutActivityTypeMap["TRACK_AND_FIELD"] = .trackAndField - workoutActivityTypeMap["AMERICAN_FOOTBALL"] = .americanFootball - workoutActivityTypeMap["AUSTRALIAN_FOOTBALL"] = .australianFootball - workoutActivityTypeMap["BASEBALL"] = .baseball - workoutActivityTypeMap["BASKETBALL"] = .basketball - workoutActivityTypeMap["CRICKET"] = .cricket - workoutActivityTypeMap["DISC_SPORTS"] = .discSports - workoutActivityTypeMap["HANDBALL"] = .handball - workoutActivityTypeMap["HOCKEY"] = .hockey - workoutActivityTypeMap["LACROSSE"] = .lacrosse - workoutActivityTypeMap["RUGBY"] = .rugby - workoutActivityTypeMap["SOCCER"] = .soccer - workoutActivityTypeMap["SOFTBALL"] = .softball - workoutActivityTypeMap["VOLLEYBALL"] = .volleyball - workoutActivityTypeMap["PREPARATION_AND_RECOVERY"] = .preparationAndRecovery - 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 - workoutActivityTypeMap["BIKING"] = .cycling - workoutActivityTypeMap["HAND_CYCLING"] = .handCycling - workoutActivityTypeMap["CORE_TRAINING"] = .coreTraining - workoutActivityTypeMap["ELLIPTICAL"] = .elliptical - workoutActivityTypeMap["FUNCTIONAL_STRENGTH_TRAINING"] = .functionalStrengthTraining - workoutActivityTypeMap["TRADITIONAL_STRENGTH_TRAINING"] = .traditionalStrengthTraining - workoutActivityTypeMap["CROSS_TRAINING"] = .crossTraining - workoutActivityTypeMap["MIXED_CARDIO"] = .mixedCardio - workoutActivityTypeMap["HIGH_INTENSITY_INTERVAL_TRAINING"] = .highIntensityIntervalTraining - workoutActivityTypeMap["JUMP_ROPE"] = .jumpRope - workoutActivityTypeMap["STAIR_CLIMBING"] = .stairClimbing - workoutActivityTypeMap["STAIRS"] = .stairs - workoutActivityTypeMap["STEP_TRAINING"] = .stepTraining - workoutActivityTypeMap["FITNESS_GAMING"] = .fitnessGaming - workoutActivityTypeMap["BARRE"] = .barre - workoutActivityTypeMap["YOGA"] = .yoga - workoutActivityTypeMap["MIND_AND_BODY"] = .mindAndBody - workoutActivityTypeMap["PILATES"] = .pilates - workoutActivityTypeMap["BADMINTON"] = .badminton - workoutActivityTypeMap["RACQUETBALL"] = .racquetball - workoutActivityTypeMap["SQUASH"] = .squash - workoutActivityTypeMap["TABLE_TENNIS"] = .tableTennis - workoutActivityTypeMap["TENNIS"] = .tennis - workoutActivityTypeMap["CLIMBING"] = .climbing - workoutActivityTypeMap["ROCK_CLIMBING"] = .climbing // Supported due to combining with Android naming - workoutActivityTypeMap["EQUESTRIAN_SPORTS"] = .equestrianSports - workoutActivityTypeMap["FISHING"] = .fishing - workoutActivityTypeMap["GOLF"] = .golf - workoutActivityTypeMap["HIKING"] = .hiking - workoutActivityTypeMap["HUNTING"] = .hunting - workoutActivityTypeMap["PLAY"] = .play - workoutActivityTypeMap["CROSS_COUNTRY_SKIING"] = .crossCountrySkiing - workoutActivityTypeMap["CURLING"] = .curling - workoutActivityTypeMap["DOWNHILL_SKIING"] = .downhillSkiing - 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["SWIMMING"] = .swimming - workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness - workoutActivityTypeMap["WATER_POLO"] = .waterPolo - workoutActivityTypeMap["WATER_SPORTS"] = .waterSports - workoutActivityTypeMap["BOXING"] = .boxing - workoutActivityTypeMap["KICKBOXING"] = .kickboxing - workoutActivityTypeMap["MARTIAL_ARTS"] = .martialArts - workoutActivityTypeMap["TAI_CHI"] = .taiChi - workoutActivityTypeMap["WRESTLING"] = .wrestling - workoutActivityTypeMap["OTHER"] = .other - - - - // Set up iOS 13 specific types (ordinary health data types) - if #available(iOS 13.0, *) { - dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)! - dataTypesDict[AUDIOGRAM] = HKSampleType.audiogramSampleType() - dataTypesDict[BASAL_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)! - dataTypesDict[BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)! - dataTypesDict[BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)! - 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[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_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)! - dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)! - dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType(forIdentifier: .electrodermalActivity)! - dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType(forIdentifier: .forcedExpiratoryVolume1)! - dataTypesDict[HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)! - dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType(forIdentifier: .heartRateVariabilitySDNN)! - dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)! - dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType(forIdentifier: .restingHeartRate)! - dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)! - dataTypesDict[WAIST_CIRCUMFERENCE] = HKSampleType.quantityType(forIdentifier: .waistCircumference)! - dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType(forIdentifier: .walkingHeartRateAverage)! - dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! - dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)! - 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_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! - dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! - dataTypesDict[WORKOUT] = HKSampleType.workoutType() - - healthDataTypes = Array(dataTypesDict.values) + + DispatchQueue.main.async { + result(dictionaries) } - // 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(forIdentifier: .highHeartRateEvent)! - dataTypesDict[LOW_HEART_RATE_EVENT] = HKSampleType.categoryType(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)!, - ]) + + case let (samplesAudiogram as [HKAudiogramSample]) as Any: + let dictionaries = samplesAudiogram.map { sample -> NSDictionary in + var frequencies = [Double]() + var leftEarSensitivities = [Double]() + var rightEarSensitivities = [Double]() + for samplePoint in sample.sensitivityPoints { + frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz())) + leftEarSensitivities.append( + samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + rightEarSensitivities.append( + samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel())) + } + return [ + "uuid": "\(sample.uuid)", + "frequencies": frequencies, + "leftEarSensitivities": leftEarSensitivities, + "rightEarSensitivities": rightEarSensitivities, + "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, + ] } - - 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)!, - ]) + DispatchQueue.main.async { + result(dictionaries) } - - 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 - workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown + + default: + if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] { + let dictionaries = ecgSamples.map(fetchEcgMeasurements) + DispatchQueue.main.async { + result(dictionaries) + } + } else { + DispatchQueue.main.async { + print("Error getting ECG - only available on iOS 14.0 and above!") + result(nil) + } } - - // Concatenate heart events, headache and health data types (both may be empty) - allDataTypes = Set(heartRateEventTypes + healthDataTypes) - allDataTypes = allDataTypes.union(headacheType) + } } -} + HKHealthStore().execute(query) + } + + @available(iOS 14.0, *) + private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary { + let semaphore = DispatchSemaphore(value: 0) + var voltageValues = [NSDictionary]() + let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in + switch result { + case let .measurement(measurement): + if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { + let voltage = voltageQuantity.doubleValue(for: HKUnit.volt()) + let timeSinceSampleStart = measurement.timeSinceSampleStart + voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart]) + } + case .done: + semaphore.signal() + case let .error(error): + print(error) + } + } + HKHealthStore().execute(voltageQuery) + semaphore.wait() + return [ + "uuid": "\(sample.uuid)", + "voltageValues": voltageValues, + "averageHeartRate": sample.averageHeartRate?.doubleValue( + for: HKUnit.count().unitDivided(by: HKUnit.minute())), + "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()), + "classification": sample.classification.rawValue, + "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, + ] + } + + 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 + + // 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( + withStart: dateFrom, end: dateTo, options: .strictStartDate) + let query = HKStatisticsQuery( + quantityType: sampleType, + quantitySamplePredicate: predicate, + options: .cumulativeSum + ) { query, queryResult, error in + + guard let queryResult = queryResult else { + let error = error! as NSError + print("Error getting total steps in interval \(error.localizedDescription)") + + DispatchQueue.main.async { + result(nil) + } + return + } + + var steps = 0.0 + + if let quantity = queryResult.sumQuantity() { + let unit = HKUnit.count() + steps = quantity.doubleValue(for: unit) + } + + let totalSteps = Int(steps) + DispatchQueue.main.async { + result(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 initializeTypes() { + // Initialize units + unitDict[GRAM] = HKUnit.gram() + unitDict[KILOGRAM] = HKUnit.gramUnit(with: .kilo) + unitDict[OUNCE] = HKUnit.ounce() + unitDict[POUND] = HKUnit.pound() + unitDict[STONE] = HKUnit.stone() + unitDict[METER] = HKUnit.meter() + unitDict[INCH] = HKUnit.inch() + unitDict[FOOT] = HKUnit.foot() + unitDict[YARD] = HKUnit.yard() + unitDict[MILE] = HKUnit.mile() + unitDict[LITER] = HKUnit.liter() + unitDict[MILLILITER] = HKUnit.literUnit(with: .milli) + unitDict[FLUID_OUNCE_US] = HKUnit.fluidOunceUS() + unitDict[FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial() + unitDict[CUP_US] = HKUnit.cupUS() + unitDict[CUP_IMPERIAL] = HKUnit.cupImperial() + unitDict[PINT_US] = HKUnit.pintUS() + unitDict[PINT_IMPERIAL] = HKUnit.pintImperial() + unitDict[PASCAL] = HKUnit.pascal() + unitDict[MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury() + unitDict[CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater() + unitDict[ATMOSPHERE] = HKUnit.atmosphere() + unitDict[DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel() + unitDict[SECOND] = HKUnit.second() + unitDict[MILLISECOND] = HKUnit.secondUnit(with: .milli) + unitDict[MINUTE] = HKUnit.minute() + unitDict[HOUR] = HKUnit.hour() + unitDict[DAY] = HKUnit.day() + unitDict[JOULE] = HKUnit.joule() + unitDict[KILOCALORIE] = HKUnit.kilocalorie() + unitDict[LARGE_CALORIE] = HKUnit.largeCalorie() + unitDict[SMALL_CALORIE] = HKUnit.smallCalorie() + unitDict[DEGREE_CELSIUS] = HKUnit.degreeCelsius() + unitDict[DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit() + unitDict[KELVIN] = HKUnit.kelvin() + unitDict[DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel() + unitDict[HERTZ] = HKUnit.hertz() + unitDict[SIEMEN] = HKUnit.siemen() + unitDict[INTERNATIONAL_UNIT] = HKUnit.internationalUnit() + unitDict[COUNT] = HKUnit.count() + unitDict[PERCENT] = HKUnit.percent() + unitDict[BEATS_PER_MINUTE] = HKUnit.init(from: "count/min") + unitDict[RESPIRATIONS_PER_MINUTE] = HKUnit.init(from: "count/min") + 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 + workoutActivityTypeMap["FENCING"] = .fencing + workoutActivityTypeMap["GYMNASTICS"] = .gymnastics + workoutActivityTypeMap["TRACK_AND_FIELD"] = .trackAndField + workoutActivityTypeMap["AMERICAN_FOOTBALL"] = .americanFootball + workoutActivityTypeMap["AUSTRALIAN_FOOTBALL"] = .australianFootball + workoutActivityTypeMap["BASEBALL"] = .baseball + workoutActivityTypeMap["BASKETBALL"] = .basketball + workoutActivityTypeMap["CRICKET"] = .cricket + workoutActivityTypeMap["DISC_SPORTS"] = .discSports + workoutActivityTypeMap["HANDBALL"] = .handball + workoutActivityTypeMap["HOCKEY"] = .hockey + workoutActivityTypeMap["LACROSSE"] = .lacrosse + workoutActivityTypeMap["RUGBY"] = .rugby + workoutActivityTypeMap["SOCCER"] = .soccer + workoutActivityTypeMap["SOFTBALL"] = .softball + workoutActivityTypeMap["VOLLEYBALL"] = .volleyball + workoutActivityTypeMap["PREPARATION_AND_RECOVERY"] = .preparationAndRecovery + 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 + workoutActivityTypeMap["BIKING"] = .cycling + workoutActivityTypeMap["HAND_CYCLING"] = .handCycling + workoutActivityTypeMap["CORE_TRAINING"] = .coreTraining + workoutActivityTypeMap["ELLIPTICAL"] = .elliptical + workoutActivityTypeMap["FUNCTIONAL_STRENGTH_TRAINING"] = .functionalStrengthTraining + workoutActivityTypeMap["TRADITIONAL_STRENGTH_TRAINING"] = .traditionalStrengthTraining + workoutActivityTypeMap["CROSS_TRAINING"] = .crossTraining + workoutActivityTypeMap["MIXED_CARDIO"] = .mixedCardio + workoutActivityTypeMap["HIGH_INTENSITY_INTERVAL_TRAINING"] = .highIntensityIntervalTraining + workoutActivityTypeMap["JUMP_ROPE"] = .jumpRope + workoutActivityTypeMap["STAIR_CLIMBING"] = .stairClimbing + workoutActivityTypeMap["STAIRS"] = .stairs + workoutActivityTypeMap["STEP_TRAINING"] = .stepTraining + workoutActivityTypeMap["FITNESS_GAMING"] = .fitnessGaming + workoutActivityTypeMap["BARRE"] = .barre + workoutActivityTypeMap["YOGA"] = .yoga + workoutActivityTypeMap["MIND_AND_BODY"] = .mindAndBody + workoutActivityTypeMap["PILATES"] = .pilates + workoutActivityTypeMap["BADMINTON"] = .badminton + workoutActivityTypeMap["RACQUETBALL"] = .racquetball + workoutActivityTypeMap["SQUASH"] = .squash + workoutActivityTypeMap["TABLE_TENNIS"] = .tableTennis + workoutActivityTypeMap["TENNIS"] = .tennis + workoutActivityTypeMap["CLIMBING"] = .climbing + workoutActivityTypeMap["ROCK_CLIMBING"] = .climbing // Supported due to combining with Android naming + workoutActivityTypeMap["EQUESTRIAN_SPORTS"] = .equestrianSports + workoutActivityTypeMap["FISHING"] = .fishing + workoutActivityTypeMap["GOLF"] = .golf + workoutActivityTypeMap["HIKING"] = .hiking + workoutActivityTypeMap["HUNTING"] = .hunting + workoutActivityTypeMap["PLAY"] = .play + workoutActivityTypeMap["CROSS_COUNTRY_SKIING"] = .crossCountrySkiing + workoutActivityTypeMap["CURLING"] = .curling + workoutActivityTypeMap["DOWNHILL_SKIING"] = .downhillSkiing + 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["SWIMMING"] = .swimming + workoutActivityTypeMap["WATER_FITNESS"] = .waterFitness + workoutActivityTypeMap["WATER_POLO"] = .waterPolo + workoutActivityTypeMap["WATER_SPORTS"] = .waterSports + workoutActivityTypeMap["BOXING"] = .boxing + workoutActivityTypeMap["KICKBOXING"] = .kickboxing + workoutActivityTypeMap["MARTIAL_ARTS"] = .martialArts + workoutActivityTypeMap["TAI_CHI"] = .taiChi + workoutActivityTypeMap["WRESTLING"] = .wrestling + workoutActivityTypeMap["OTHER"] = .other + + // Set up iOS 13 specific types (ordinary health data types) + if #available(iOS 13.0, *) { + dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType( + forIdentifier: .activeEnergyBurned)! + dataTypesDict[AUDIOGRAM] = HKSampleType.audiogramSampleType() + dataTypesDict[BASAL_ENERGY_BURNED] = HKSampleType.quantityType( + forIdentifier: .basalEnergyBurned)! + dataTypesDict[BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)! + dataTypesDict[BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)! + 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[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_FATS_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryFatTotal)! + dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType( + forIdentifier: .dietaryProtein)! + dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType( + forIdentifier: .electrodermalActivity)! + dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType( + forIdentifier: .forcedExpiratoryVolume1)! + dataTypesDict[HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)! + dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType( + forIdentifier: .heartRateVariabilitySDNN)! + dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)! + dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType( + forIdentifier: .restingHeartRate)! + dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)! + dataTypesDict[WAIST_CIRCUMFERENCE] = HKSampleType.quantityType( + forIdentifier: .waistCircumference)! + dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType( + forIdentifier: .walkingHeartRateAverage)! + dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)! + dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType( + forIdentifier: .distanceWalkingRunning)! + 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_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)! + + dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)! + dataTypesDict[WORKOUT] = HKSampleType.workoutType() + + healthDataTypes = Array(dataTypesDict.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( + forIdentifier: .highHeartRateEvent)! + dataTypesDict[LOW_HEART_RATE_EVENT] = HKSampleType.categoryType( + 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 + workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown + } + + // Concatenate heart events, headache and health data types (both may be empty) + allDataTypes = Set(heartRateEventTypes + healthDataTypes) + allDataTypes = allDataTypes.union(headacheType) + } +} diff --git a/packages/health/lib/src/data_types.dart b/packages/health/lib/src/data_types.dart index 357585ffc..ee952d093 100644 --- a/packages/health/lib/src/data_types.dart +++ b/packages/health/lib/src/data_types.dart @@ -21,6 +21,8 @@ enum HealthDataType { HEART_RATE_VARIABILITY_SDNN, HEIGHT, RESTING_HEART_RATE, + RESPIRATORY_RATE, + PERIPHERAL_PERFUSION_INDEX, STEPS, WAIST_CIRCUMFERENCE, WALKING_HEART_RATE, @@ -34,6 +36,11 @@ enum HealthDataType { SLEEP_IN_BED, SLEEP_ASLEEP, SLEEP_AWAKE, + SLEEP_LIGHT, + SLEEP_DEEP, + SLEEP_REM, + SLEEP_OUT_OF_BED, + SLEEP_SESSION, EXERCISE_TIME, WORKOUT, HEADACHE_NOT_PRESENT, @@ -51,6 +58,7 @@ enum HealthDataType { ELECTROCARDIOGRAM, } +/// Access types for Health Data. enum HealthDataAccess { READ, WRITE, @@ -82,6 +90,8 @@ const List _dataTypeKeysIOS = [ HealthDataType.IRREGULAR_HEART_RATE_EVENT, HealthDataType.LOW_HEART_RATE_EVENT, HealthDataType.RESTING_HEART_RATE, + HealthDataType.RESPIRATORY_RATE, + HealthDataType.PERIPHERAL_PERFUSION_INDEX, HealthDataType.STEPS, HealthDataType.WAIST_CIRCUMFERENCE, HealthDataType.WALKING_HEART_RATE, @@ -92,6 +102,8 @@ const List _dataTypeKeysIOS = [ HealthDataType.SLEEP_IN_BED, HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_REM, HealthDataType.WATER, HealthDataType.EXERCISE_TIME, HealthDataType.WORKOUT, @@ -121,9 +133,17 @@ const List _dataTypeKeysAndroid = [ HealthDataType.DISTANCE_DELTA, HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, - HealthDataType.SLEEP_IN_BED, + HealthDataType.SLEEP_DEEP, + HealthDataType.SLEEP_LIGHT, + HealthDataType.SLEEP_REM, + HealthDataType.SLEEP_OUT_OF_BED, + HealthDataType.SLEEP_SESSION, HealthDataType.WATER, HealthDataType.WORKOUT, + HealthDataType.RESTING_HEART_RATE, + HealthDataType.FLIGHTS_CLIMBED, + HealthDataType.BASAL_ENERGY_BURNED, + HealthDataType.RESPIRATORY_RATE, HealthDataType.NUTRITION, ]; @@ -146,6 +166,8 @@ const Map _dataTypeToUnit = { 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.RESTING_HEART_RATE: HealthDataUnit.BEATS_PER_MINUTE, HealthDataType.STEPS: HealthDataUnit.COUNT, @@ -161,6 +183,12 @@ const Map _dataTypeToUnit = { HealthDataType.SLEEP_IN_BED: HealthDataUnit.MINUTE, HealthDataType.SLEEP_ASLEEP: HealthDataUnit.MINUTE, HealthDataType.SLEEP_AWAKE: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_DEEP: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_REM: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_OUT_OF_BED: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_LIGHT: HealthDataUnit.MINUTE, + HealthDataType.SLEEP_SESSION: HealthDataUnit.MINUTE, + HealthDataType.MINDFULNESS: HealthDataUnit.MINUTE, HealthDataType.EXERCISE_TIME: HealthDataUnit.MINUTE, HealthDataType.WORKOUT: HealthDataUnit.NO_UNIT, @@ -261,6 +289,7 @@ enum HealthDataUnit { // Other units BEATS_PER_MINUTE, + RESPIRATIONS_PER_MINUTE, MILLIGRAM_PER_DECILITER, UNKNOWN_UNIT, NO_UNIT, @@ -425,6 +454,7 @@ enum HealthWorkoutActivityType { OTHER, } +/// Classifications for ECG readings. enum ElectrocardiogramClassification { NOT_SET, SINUS_RHYTHM, @@ -436,6 +466,7 @@ enum ElectrocardiogramClassification { UNRECOGNIZED, } +/// Extension to assign numbers to [ElectrocardiogramClassification]s extension ElectrocardiogramClassificationValue on ElectrocardiogramClassification { int get value { diff --git a/packages/health/lib/src/functions.dart b/packages/health/lib/src/functions.dart index db9bd548a..24d191fc6 100644 --- a/packages/health/lib/src/functions.dart +++ b/packages/health/lib/src/functions.dart @@ -3,7 +3,10 @@ part of health; /// Custom Exception for the plugin. Used when a Health Data Type is requested, /// but not available on the current platform. class HealthException implements Exception { + /// Data Type that was requested. dynamic dataType; + + /// Cause of the exception. String cause; HealthException(this.dataType, this.cause); diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 62d8749b9..e6c916467 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -33,11 +33,16 @@ class HealthDataPoint { type == HealthDataType.HEADACHE_SEVERE || type == HealthDataType.SLEEP_IN_BED || type == HealthDataType.SLEEP_ASLEEP || - type == HealthDataType.SLEEP_AWAKE) { + type == HealthDataType.SLEEP_AWAKE || + type == HealthDataType.SLEEP_DEEP || + type == HealthDataType.SLEEP_LIGHT || + type == HealthDataType.SLEEP_REM || + type == HealthDataType.SLEEP_OUT_OF_BED) { this._value = _convertMinutes(); } } + /// Converts dateTo - dateFrom to minutes. NumericHealthValue _convertMinutes() { int ms = dateTo.millisecondsSinceEpoch - dateFrom.millisecondsSinceEpoch; return NumericHealthValue(ms / (1000 * 60)); @@ -84,7 +89,7 @@ class HealthDataPoint { }; @override - String toString() => """${this.runtimeType} - + String toString() => """${this.runtimeType} - value: ${value.toString()}, unit: ${unit.name}, dateFrom: $dateFrom, @@ -95,7 +100,7 @@ class HealthDataPoint { sourceId: $sourceId, sourceName: $sourceName"""; - // / The quantity value of the data point + /// The quantity value of the data point HealthValue get value => _value; /// The start of the time interval diff --git a/packages/health/lib/src/health_factory.dart b/packages/health/lib/src/health_factory.dart index 7717a5c3c..08033980b 100644 --- a/packages/health/lib/src/health_factory.dart +++ b/packages/health/lib/src/health_factory.dart @@ -14,10 +14,20 @@ class HealthFactory { static const MethodChannel _channel = MethodChannel('flutter_health'); String? _deviceId; final _deviceInfo = DeviceInfoPlugin(); + late bool _useHealthConnectIfAvailable; static PlatformType _platformType = Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS; + /// The plugin was created to use Health Connect (if true) or Google Fit (if false). + bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; + + HealthFactory({bool useHealthConnectIfAvailable = false}) { + _useHealthConnectIfAvailable = useHealthConnectIfAvailable; + if (_useHealthConnectIfAvailable) + _channel.invokeMethod('useHealthConnectIfAvailable'); + } + /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => _platformType == PlatformType.ANDROID @@ -47,7 +57,7 @@ class HealthFactory { /// with a READ or READ_WRITE access. /// /// On Android, this function returns true or false, depending on whether the specified access right has been granted. - static Future hasPermissions(List types, + Future hasPermissions(List types, {List? permissions}) async { if (permissions != null && permissions.length != types.length) throw ArgumentError( @@ -68,11 +78,21 @@ class HealthFactory { }); } - /// Revoke permissions obtained earlier. + /// Revokes permissions of all types. + /// Uses `disableFit()` on Google Fit. /// - /// Not supported on iOS and method does nothing. - static Future revokePermissions() async { - return await _channel.invokeMethod('revokePermissions'); + /// Not implemented on iOS as there is no way to programmatically remove access. + Future revokePermissions() async { + try { + if (_platformType == PlatformType.IOS) { + throw UnsupportedError( + 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); + } + await _channel.invokeMethod('revokePermissions'); + return; + } catch (e) { + print(e); + } } /// Requests permissions to access data types in Apple Health or Google Fit. @@ -105,10 +125,14 @@ class HealthFactory { for (int i = 0; i < types.length; i++) { final type = types[i]; final permission = permissions[i]; - if (type == HealthDataType.ELECTROCARDIOGRAM && + if ((type == HealthDataType.ELECTROCARDIOGRAM || + type == HealthDataType.HIGH_HEART_RATE_EVENT || + type == HealthDataType.LOW_HEART_RATE_EVENT || + type == HealthDataType.IRREGULAR_HEART_RATE_EVENT || + type == HealthDataType.WALKING_HEART_RATE) && permission != HealthDataAccess.READ) { throw ArgumentError( - 'Requesting WRITE permission on ELECTROCARDIOGRAM is not allowed.'); + 'Requesting WRITE permission on ELECTROCARDIOGRAM / HIGH_HEART_RATE_EVENT / LOW_HEART_RATE_EVENT / IRREGULAR_HEART_RATE_EVENT / WALKING_HEART_RATE is not allowed.'); } } } @@ -128,6 +152,7 @@ class HealthFactory { return isAuthorized ?? false; } + /// Obtains health and weight if BMI is requested on Android. static void _handleBMI(List mTypes, List mPermissions) { final index = mTypes.indexOf(HealthDataType.BODY_MASS_INDEX); @@ -197,6 +222,7 @@ class HealthFactory { /// * [endTime] - the end time when this [value] is measured. /// + 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. + /// * [unit] - (iOS ONLY) the unit the health data is measured in. /// /// Values for Sleep and Headache are ignored and will be automatically assigned the coresponding value. Future writeHealthData( @@ -212,11 +238,12 @@ class HealthFactory { if (startTime.isAfter(endTime)) throw ArgumentError("startTime must be equal or earlier than endTime"); if ({ - HealthDataType.HIGH_HEART_RATE_EVENT, - HealthDataType.LOW_HEART_RATE_EVENT, - HealthDataType.IRREGULAR_HEART_RATE_EVENT, - HealthDataType.ELECTROCARDIOGRAM, - }.contains(type)) + HealthDataType.HIGH_HEART_RATE_EVENT, + HealthDataType.LOW_HEART_RATE_EVENT, + HealthDataType.IRREGULAR_HEART_RATE_EVENT, + HealthDataType.ELECTROCARDIOGRAM, + }.contains(type) && + _platformType == PlatformType.IOS) throw ArgumentError( "$type - iOS doesnt support writing this data type in HealthKit"); @@ -228,6 +255,8 @@ class HealthFactory { if (type == HealthDataType.SLEEP_ASLEEP || type == HealthDataType.SLEEP_AWAKE || type == HealthDataType.SLEEP_IN_BED || + type == HealthDataType.SLEEP_DEEP || + type == HealthDataType.SLEEP_REM || type == HealthDataType.HEADACHE_NOT_PRESENT || type == HealthDataType.HEADACHE_MILD || type == HealthDataType.HEADACHE_MODERATE || @@ -298,6 +327,41 @@ class HealthFactory { return success ?? false; } + /// Saves blood oxygen saturation record into Apple Health or Google Fit/Health Connect. + /// + /// Returns true if successful, false otherwise. + /// + /// 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 [value] is measured. + /// + It must be equal to or earlier than [endTime]. + /// * [endTime] - the end time when this [value] is measured. + /// + It 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. + Future writeBloodOxygen( + double saturation, DateTime startTime, DateTime endTime, + {double flowRate = 0.0}) async { + if (startTime.isAfter(endTime)) + throw ArgumentError("startTime must be equal or earlier than endTime"); + bool? success; + + if (_platformType == PlatformType.IOS) { + success = await writeHealthData( + saturation, HealthDataType.BLOOD_OXYGEN, startTime, endTime); + } else if (_platformType == PlatformType.ANDROID) { + Map args = { + 'value': saturation, + 'flowRate': flowRate, + 'startTime': startTime.millisecondsSinceEpoch, + 'endTime': endTime.millisecondsSinceEpoch, + 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, + }; + success = await _channel.invokeMethod('writeBloodOxygen', args); + } + return success ?? false; + } + /// Saves meal record into Apple Health or Google Fit. /// /// Returns true if successful, false otherwise. @@ -424,7 +488,7 @@ class HealthFactory { return await _dataQuery(startTime, endTime, dataType); } - /// The main function for fetching health data + /// Fetches data points from Android/iOS native code. Future> _dataQuery( DateTime startTime, DateTime endTime, HealthDataType dataType) async { final args = { @@ -434,6 +498,7 @@ class HealthFactory { 'endTime': endTime.millisecondsSinceEpoch }; final fetchedDataPoints = await _channel.invokeMethod('getData', args); + if (fetchedDataPoints != null) { final mesg = { "dataType": dataType, @@ -452,6 +517,7 @@ class HealthFactory { } } + /// Parses the fetched data points into a list of [HealthDataPoint]. static List _parse(Map message) { final dataType = message["dataType"]; final dataPoints = message["dataPoints"]; @@ -514,14 +580,19 @@ class HealthFactory { return stepsCount; } + /// Assigns numbers to specific [HealthDataType]s. int _alignValue(HealthDataType type) { switch (type) { case HealthDataType.SLEEP_IN_BED: return 0; - case HealthDataType.SLEEP_ASLEEP: - return 1; case HealthDataType.SLEEP_AWAKE: return 2; + case HealthDataType.SLEEP_ASLEEP: + return 3; + case HealthDataType.SLEEP_DEEP: + return 4; + case HealthDataType.SLEEP_REM: + return 5; case HealthDataType.HEADACHE_UNSPECIFIED: return 0; case HealthDataType.HEADACHE_NOT_PRESENT: diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index 526d69870..a61d5c7d8 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -11,6 +11,7 @@ class NumericHealthValue extends HealthValue { NumericHealthValue(this._numericValue); + /// A [num] value for the [HealthDataPoint]. num get numericValue => _numericValue; @override @@ -18,6 +19,7 @@ class NumericHealthValue extends HealthValue { return numericValue.toString(); } + /// Parses a json object to [NumericHealthValue] factory NumericHealthValue.fromJson(json) { return NumericHealthValue(num.parse(json['numericValue'])); } @@ -49,14 +51,19 @@ class AudiogramHealthValue extends HealthValue { AudiogramHealthValue(this._frequencies, this._leftEarSensitivities, this._rightEarSensitivities); + /// Array of frequencies of the test. List get frequencies => _frequencies; + + /// Threshold in decibel for the left ear. List get leftEarSensitivities => _leftEarSensitivities; + + /// Threshold in decibel for the right ear. List get rightEarSensitivities => _rightEarSensitivities; @override String toString() { - return """frequencies: ${frequencies.toString()}, - left ear sensitivities: ${leftEarSensitivities.toString()}, + return """frequencies: ${frequencies.toString()}, + left ear sensitivities: ${leftEarSensitivities.toString()}, right ear sensitivities: ${rightEarSensitivities.toString()}"""; } @@ -183,14 +190,21 @@ class WorkoutHealthValue extends HealthValue { /// A [HealthValue] object for ECGs /// /// Parameters: -/// * [voltageValues] - an array of [ElectrocardiogramVoltageValue] +/// * [voltageValues] - an array of [ElectrocardiogramVoltageValue]s /// * [averageHeartRate] - the average heart rate during the ECG (in BPM) /// * [samplingFrequency] - the frequency at which the Apple Watch sampled the voltage. /// * [classification] - an [ElectrocardiogramClassification] class ElectrocardiogramHealthValue extends HealthValue { + /// An array of [ElectrocardiogramVoltageValue]s. List voltageValues; + + /// The average heart rate during the ECG (in BPM). num? averageHeartRate; + + /// The frequency at which the Apple Watch sampled the voltage. double? samplingFrequency; + + /// An [ElectrocardiogramClassification]. ElectrocardiogramClassification classification; ElectrocardiogramHealthValue({ @@ -200,6 +214,7 @@ class ElectrocardiogramHealthValue extends HealthValue { required this.classification, }); + /// Parses [ElectrocardiogramHealthValue] from JSON. factory ElectrocardiogramHealthValue.fromJson(json) => ElectrocardiogramHealthValue( voltageValues: (json['voltageValues'] as List) @@ -238,7 +253,10 @@ class ElectrocardiogramHealthValue extends HealthValue { /// Single voltage value belonging to a [ElectrocardiogramHealthValue] class ElectrocardiogramVoltageValue extends HealthValue { + /// Voltage of the ECG. num voltage; + + /// Time since the start of the ECG. num timeSinceSampleStart; ElectrocardiogramVoltageValue(this.voltage, this.timeSinceSampleStart); @@ -265,6 +283,7 @@ class ElectrocardiogramVoltageValue extends HealthValue { String toString() => voltage.toString(); } +/// An abstract class for health values. abstract class HealthValue { Map toJson(); } diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 92f037cd4..28ce3128f 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,17 +1,17 @@ name: health -description: Wrapper for the iOS HealthKit and Android GoogleFit services. -version: 4.5.0 +description: Wrapper for HealthKit on iOS and Google Fit and Health Connect on Android. +version: 8.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.0.0" dependencies: flutter: sdk: flutter intl: ^0.18.0 - device_info_plus: ^8.0.0 + device_info_plus: ^9.0.0 dev_dependencies: flutter_test: diff --git a/packages/light/CHANGELOG.md b/packages/light/CHANGELOG.md index 76a4c7f64..d5872acc9 100644 --- a/packages/light/CHANGELOG.md +++ b/packages/light/CHANGELOG.md @@ -1,12 +1,27 @@ +## 3.0.1 + +- Updated pubspec.yaml + +## 3.0.0 + +- `Light()` implemented as singleton. +- Updates Kotlin plugin and AGP. +- Upgrade of `compileSdkVersion` to 33. +- Upgrade to Dart 3. + ## 2.1.0 + - `lightSensorStream` is not returning `null` values. - update to example app ## 2.0.0 + - Migration to null safety ## 1.0.1 + - fix: issue [#139](https://github.com/cph-cachet/carp.sensing-flutter/issues/139) ## 1.0.0 + - Updated version of the old plugin. diff --git a/packages/light/README.md b/packages/light/README.md index 5e8c24a83..b12414e6d 100644 --- a/packages/light/README.md +++ b/packages/light/README.md @@ -1,14 +1,38 @@ -# light +# Light -Plugin for the light sensor (Android only) +[![pub package](https://img.shields.io/pub/v/light.svg)](https://pub.dartlang.org/packages/light) -## Getting Started +A Flutter plugin for collecting data from the [ambient light sensor on Android](https://developer.android.com/guide/topics/sensors/sensors_environment#java). -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +## Install -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Add `light` as a dependency in the `pubspec.yaml` file. + +For help on adding as a dependency, view the [documentation](https://flutter.io/using-packages/). + +## Usage + +Instantiate an instance of the `Light()` plugin and listen on the `lightSensorStream` stream. + +```dart + Light? _light; + StreamSubscription? _subscription; + + void onData(int luxValue) async { + print("Lux value: $luxValue"); + } + + + void startListening() { + _light = Light(); + try { + _subscription = _light?.lightSensorStream.listen(onData); + } on LightException catch (exception) { + print(exception); + } + } + + void stopListening() { + _subscription?.cancel(); + } +``` diff --git a/packages/light/android/build.gradle b/packages/light/android/build.gradle index bd975b49e..99f377894 100644 --- a/packages/light/android/build.gradle +++ b/packages/light/android/build.gradle @@ -1,32 +1,51 @@ group 'dk.cachet.light' -version '1.0' +version '1.0-SNAPSHOT' buildscript { + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -rootProject.allprojects { +allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion 31 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + defaultConfig { minSdkVersion 16 } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + lintOptions { disable 'InvalidPackage' } diff --git a/packages/light/example/.flutter-plugins-dependencies b/packages/light/example/.flutter-plugins-dependencies deleted file mode 100644 index f7b0626cc..000000000 --- a/packages/light/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"light","path":"/Users/rexios/repos/flutter-plugins/packages/light/","dependencies":[]}],"android":[{"name":"light","path":"/Users/rexios/repos/flutter-plugins/packages/light/","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"light","dependencies":[]}],"date_created":"2022-02-17 17:46:17.949548","version":"2.10.1"} \ No newline at end of file diff --git a/packages/light/example/README.md b/packages/light/example/README.md index 69e32388c..19ec7524c 100644 --- a/packages/light/example/README.md +++ b/packages/light/example/README.md @@ -1,16 +1,3 @@ -# light_example +# Light Plugin Example -Demonstrates how to use the light plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Demonstrates how to use the `light` plugin. diff --git a/packages/light/example/android/app/build.gradle b/packages/light/example/android/app/build.gradle index 1bc240d4e..e25e15a13 100644 --- a/packages/light/example/android/app/build.gradle +++ b/packages/light/example/android/app/build.gradle @@ -25,14 +25,25 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + namespace "dk.cachet.light_example" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + 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.light_example" - minSdkVersion 16 - targetSdkVersion 31 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/light/example/android/app/src/main/AndroidManifest.xml b/packages/light/example/android/app/src/main/AndroidManifest.xml index f39d69bce..f62dbc396 100644 --- a/packages/light/example/android/app/src/main/AndroidManifest.xml +++ b/packages/light/example/android/app/src/main/AndroidManifest.xml @@ -20,15 +20,6 @@ android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> - - diff --git a/packages/light/example/android/build.gradle b/packages/light/example/android/build.gradle index 86eb13622..f7eb7f63c 100644 --- a/packages/light/example/android/build.gradle +++ b/packages/light/example/android/build.gradle @@ -1,18 +1,20 @@ buildscript { + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -24,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/light/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/light/example/android/gradle/wrapper/gradle-wrapper.properties index 595fb867a..6b665338b 100644 --- a/packages/light/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/light/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/light/example/ios/Flutter/flutter_export_environment.sh b/packages/light/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index 260799c99..000000000 --- a/packages/light/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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/light/example" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=false" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.packages" diff --git a/packages/light/example/lib/main.dart b/packages/light/example/lib/main.dart index 4c2df8ea1..90a3bb339 100644 --- a/packages/light/example/lib/main.dart +++ b/packages/light/example/lib/main.dart @@ -11,8 +11,8 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { String _luxString = 'Unknown'; - late Light _light; - late StreamSubscription _subscription; + Light? _light; + StreamSubscription? _subscription; void onData(int luxValue) async { print("Lux value: $luxValue"); @@ -22,13 +22,13 @@ class _MyAppState extends State { } void stopListening() { - _subscription.cancel(); + _subscription?.cancel(); } void startListening() { - _light = new Light(); + _light = Light(); try { - _subscription = _light.lightSensorStream.listen(onData); + _subscription = _light?.lightSensorStream.listen(onData); } on LightException catch (exception) { print(exception); } @@ -37,11 +37,6 @@ class _MyAppState extends State { @override void initState() { super.initState(); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { startListening(); } diff --git a/packages/light/example/pubspec.yaml b/packages/light/example/pubspec.yaml index b5d64b79a..6f11852c2 100644 --- a/packages/light/example/pubspec.yaml +++ b/packages/light/example/pubspec.yaml @@ -1,12 +1,9 @@ name: light_example description: Demonstrates how to use the light 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 +publish_to: "none" environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: flutter: @@ -20,8 +17,6 @@ dependencies: # 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 dev_dependencies: diff --git a/packages/light/example/test/widget_test.dart b/packages/light/example/test/widget_test.dart index 74d8b98c9..421225d18 100644 --- a/packages/light/example/test/widget_test.dart +++ b/packages/light/example/test/widget_test.dart @@ -18,8 +18,8 @@ void main() { // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); diff --git a/packages/light/lib/light.dart b/packages/light/lib/light.dart index 14d4311be..2931d38a0 100644 --- a/packages/light/lib/light.dart +++ b/packages/light/lib/light.dart @@ -13,22 +13,26 @@ class LightException implements Exception { } class Light { + static Light? _singleton; static const EventChannel _eventChannel = const EventChannel("light.eventChannel"); + /// Constructs a singleton instance of [Light]. + /// + /// [Light] is designed to work as a singleton. + factory Light() => _singleton ??= Light._(); + + Light._(); + Stream? _lightSensorStream; - /// A stream of light events. - /// Throws an exception if device isn't on Android. + /// The stream of light events. + /// Throws a [LightException] if device isn't on Android. Stream get lightSensorStream { - if (Platform.isAndroid) { - if (_lightSensorStream == null) { - _lightSensorStream = - _eventChannel.receiveBroadcastStream().map((lux) => lux); - } - return _lightSensorStream!; - } - - throw LightException('Light sensor API exclusively available on Android!'); + if (!Platform.isAndroid) + throw LightException('Light sensor API only available on Android.'); + + return _lightSensorStream ??= + _eventChannel.receiveBroadcastStream().map((lux) => lux); } } diff --git a/packages/light/pubspec.yaml b/packages/light/pubspec.yaml index 90047f0cb..00bbc875f 100644 --- a/packages/light/pubspec.yaml +++ b/packages/light/pubspec.yaml @@ -1,21 +1,17 @@ name: light -description: Plugin for the light sensor (Android only) -version: 2.1.0 +description: Plugin for collecting data from the ambient light sensor on Android. +version: 3.0.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -23,4 +19,3 @@ flutter: pluginClass: LightPlugin ios: pluginClass: LightPlugin - diff --git a/packages/mobility_features/CHANGELOG.md b/packages/mobility_features/CHANGELOG.md index 467662852..7555c34ad 100644 --- a/packages/mobility_features/CHANGELOG.md +++ b/packages/mobility_features/CHANGELOG.md @@ -1,84 +1,114 @@ +## 4.0.1 + +- Fixed formatting +- Lowered minSdk + +## 4.0.0 + +- 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 -## 2.0.6+1 -* removal of exception -* update to use `carp_background_location` +- 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` ## 2.0.5 -* 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 +- 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 ## 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/example/.flutter-plugins-dependencies b/packages/mobility_features/example/.flutter-plugins-dependencies deleted file mode 100644 index 4c3dcc7f2..000000000 --- a/packages/mobility_features/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"background_locator","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/background_locator-1.6.12/","dependencies":[]},{"name":"path_provider_ios","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/path_provider_ios-2.0.7/","dependencies":[]}],"android":[{"name":"background_locator","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/background_locator-1.6.12/","dependencies":[]},{"name":"path_provider_android","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/path_provider_android-2.0.11/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-2.0.4/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-2.1.4/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/bardram/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-2.0.4/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"background_locator","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_ios","path_provider_linux","path_provider_macos","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_ios","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2022-01-09 19:33:13.611264","version":"2.5.3"} \ No newline at end of file diff --git a/packages/mobility_features/example/android/app/build.gradle b/packages/mobility_features/example/android/app/build.gradle index dcb69350b..e5ef93d83 100644 --- a/packages/mobility_features/example/android/app/build.gradle +++ b/packages/mobility_features/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +39,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion 23 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } 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 bd1b230f6..df58824d8 100644 --- a/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml +++ b/packages/mobility_features/example/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + + + android:label="Pedometer"> - - diff --git a/packages/pedometer/example/android/build.gradle b/packages/pedometer/example/android/build.gradle index 3100ad2d5..f7eb7f63c 100644 --- a/packages/pedometer/example/android/build.gradle +++ b/packages/pedometer/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/pedometer/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/pedometer/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..78a9a9174 100644 --- a/packages/pedometer/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/pedometer/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip diff --git a/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh b/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh index 29b93e819..be27e1299 100755 --- a/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh +++ b/packages/pedometer/example/ios/Flutter/flutter_export_environment.sh @@ -1,14 +1,13 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/madsvedelsaabychristensen/Development/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/madsvedelsaabychristensen/Development/CACHET/flutter-plugins/packages/pedometer/example" -export "FLUTTER_TARGET=/Users/madsvedelsaabychristensen/Development/CACHET/flutter-plugins/packages/pedometer/example/lib/main.dart" +export "FLUTTER_ROOT=/Users/bardram/dev/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/bardram/dev/flutter-plugins/packages/pedometer/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=flutter.inspector.structuredErrors%3Dtrue" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=/Users/madsvedelsaabychristensen/Development/CACHET/flutter-plugins/packages/pedometer/example/.dart_tool/package_config.json" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/packages/pedometer/example/lib/main.dart b/packages/pedometer/example/lib/main.dart index 35dcb4c32..1310d2c6e 100644 --- a/packages/pedometer/example/lib/main.dart +++ b/packages/pedometer/example/lib/main.dart @@ -73,14 +73,14 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('Pedometer example app'), + title: const Text('Pedometer Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Steps taken:', + 'Steps Taken', style: TextStyle(fontSize: 30), ), Text( @@ -93,7 +93,7 @@ class _MyAppState extends State { color: Colors.white, ), Text( - 'Pedestrian status:', + 'Pedestrian Status', style: TextStyle(fontSize: 30), ), Icon( diff --git a/packages/pedometer/example/pubspec.yaml b/packages/pedometer/example/pubspec.yaml index 7b843ecc5..725f653f4 100644 --- a/packages/pedometer/example/pubspec.yaml +++ b/packages/pedometer/example/pubspec.yaml @@ -1,12 +1,9 @@ name: pedometer_example description: Demonstrates how to use the pedometer 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 +publish_to: "none" environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' dependencies: flutter: @@ -20,51 +17,11 @@ dependencies: # 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 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 diff --git a/packages/pedometer/example/test/widget_test.dart b/packages/pedometer/example/test/widget_test.dart index 12c408ddb..dc87413b6 100644 --- a/packages/pedometer/example/test/widget_test.dart +++ b/packages/pedometer/example/test/widget_test.dart @@ -18,8 +18,8 @@ void main() { // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); diff --git a/packages/pedometer/pubspec.yaml b/packages/pedometer/pubspec.yaml index 3d59e4e18..f676ff350 100644 --- a/packages/pedometer/pubspec.yaml +++ b/packages/pedometer/pubspec.yaml @@ -1,11 +1,11 @@ name: pedometer description: A Pedometer and Step Detection package for Android and iOS. Step count is streamed as the platform updates it. -version: 3.0.0 +version: 4.0.1 homepage: https://github.com/cph-cachet/flutter-plugins environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -15,15 +15,7 @@ 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: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -31,34 +23,3 @@ flutter: pluginClass: PedometerPlugin ios: pluginClass: PedometerPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, 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 in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/screen_state/CHANGELOG.md b/packages/screen_state/CHANGELOG.md index c2934085a..2d0b19d97 100644 --- a/packages/screen_state/CHANGELOG.md +++ b/packages/screen_state/CHANGELOG.md @@ -1,3 +1,14 @@ +## 3.0.1 + +- Reduced minSdk version to 23 + +## 3.0.0 + +- `Screen()` implemented as singleton. +- Updates Kotlin plugin and AGP. +- Upgrade of `compileSdkVersion` to 33. +- Upgrade to Dart 3. + ## 2.0.0 - Null safety migration @@ -5,7 +16,7 @@ ## 1.0.1 - Fixed an issue causing the plugin to crash when the stream was cancelled and then opened again. - - See https://github.com/cph-cachet/flutter-plugins/issues/188 + - See ## 1.0.0 diff --git a/packages/screen_state/android/build.gradle b/packages/screen_state/android/build.gradle index f35e27efe..d7a23c725 100644 --- a/packages/screen_state/android/build.gradle +++ b/packages/screen_state/android/build.gradle @@ -2,14 +2,14 @@ group 'dk.cachet.screen_state' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.20' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 23 } lintOptions { disable 'InvalidPackage' diff --git a/packages/screen_state/example/android/app/build.gradle b/packages/screen_state/example/android/app/build.gradle index d9f21c7a6..40d90dec2 100644 --- a/packages/screen_state/example/android/app/build.gradle +++ b/packages/screen_state/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +39,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dk.cachet.screen_state_example" - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion 23 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } 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 99a59143c..84a90baf1 100644 --- a/packages/screen_state/example/android/app/src/main/AndroidManifest.xml +++ b/packages/screen_state/example/android/app/src/main/AndroidManifest.xml @@ -6,11 +6,12 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> '} + 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=`basename "$0"` +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" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -85,76 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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 -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -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" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=$((i+1)) + # 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 - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# 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 index 8a0b282aa..107acd32c 100644 --- a/packages/screen_state/example/android/gradlew.bat +++ b/packages/screen_state/example/android/gradlew.bat @@ -1,3 +1,19 @@ +@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 @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@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= - 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 init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :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 %CMD_LINE_ARGS% +"%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 diff --git a/packages/screen_state/example/lib/main.dart b/packages/screen_state/example/lib/main.dart index 1dfb286a4..38e2be0d2 100644 --- a/packages/screen_state/example/lib/main.dart +++ b/packages/screen_state/example/lib/main.dart @@ -21,38 +21,35 @@ class ScreenStateEventEntry { class _MyAppState extends State { Screen _screen = Screen(); - late StreamSubscription _subscription; + StreamSubscription? _subscription; bool started = false; List _log = []; void initState() { super.initState(); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { startListening(); } - void onData(ScreenStateEvent event) { - setState(() { - _log.add(ScreenStateEventEntry(event)); - }); - print(event); - } - + /// Start listening to screen events void startListening() { try { - _subscription = _screen.screenStateStream!.listen(onData); + _subscription = _screen.screenStateStream!.listen(_onData); setState(() => started = true); } on ScreenStateException catch (exception) { print(exception); } } + void _onData(ScreenStateEvent event) { + setState(() { + _log.add(ScreenStateEventEntry(event)); + }); + print(event); + } + + /// Stop listening to screen events void stopListening() { - _subscription.cancel(); + _subscription?.cancel(); setState(() => started = false); } @@ -61,7 +58,7 @@ class _MyAppState extends State { return new MaterialApp( home: new Scaffold( appBar: new AppBar( - title: const Text('Screen State Example app'), + title: const Text('Screen State Example'), ), body: new Center( child: new ListView.builder( @@ -75,7 +72,7 @@ class _MyAppState extends State { })), floatingActionButton: new FloatingActionButton( onPressed: started ? stopListening : startListening, - tooltip: 'Start/Stop sensing', + tooltip: 'Start/Stop Listening', child: started ? Icon(Icons.stop) : Icon(Icons.play_arrow), ), ), diff --git a/packages/screen_state/example/pubspec.yaml b/packages/screen_state/example/pubspec.yaml index 1c5760d5d..4f32c97c4 100644 --- a/packages/screen_state/example/pubspec.yaml +++ b/packages/screen_state/example/pubspec.yaml @@ -6,7 +6,7 @@ description: Demonstrates how to use the screen_state plugin. publish_to: "none" # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.17.0 <4.0.0" dependencies: flutter: diff --git a/packages/screen_state/example/test/widget_test.dart b/packages/screen_state/example/test/widget_test.dart deleted file mode 100644 index 75197b729..000000000 --- a/packages/screen_state/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:screen_state_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/packages/screen_state/lib/screen_state.dart b/packages/screen_state/lib/screen_state.dart index 7dbb56801..920caa70d 100644 --- a/packages/screen_state/lib/screen_state.dart +++ b/packages/screen_state/lib/screen_state.dart @@ -2,27 +2,33 @@ import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/services.dart'; -/// Enumeration of Screen State events coming from android. +/// The type of screen state events coming from Android. enum ScreenStateEvent { SCREEN_UNLOCKED, SCREEN_ON, SCREEN_OFF } -/// Custom Exception for the plugin, -/// thrown whenever the plugin is used on platforms other than Android +/// Custom Exception for the `screen_state` plugin, used whenever the plugin +/// is used on platforms other than Android class ScreenStateException implements Exception { String _cause; ScreenStateException(this._cause); @override - String toString() { - return _cause; - } + String toString() => '$runtimeType - $_cause'; } /// Screen representation as object which holds the stream for [ScreenStateEvent]s. class Screen { + static Screen? _singleton; EventChannel _eventChannel = const EventChannel('screenStateEvents'); Stream? _screenStateStream; + /// Constructs a singleton instance of [Screen]. + /// + /// [Screen] is designed to work as a singleton. + factory Screen() => _singleton ??= Screen._(); + + Screen._(); + /// Stream of [ScreenStateEvent]s. /// Each event is streamed as it occurs on the phone. /// Only Android [ScreenStateEvent] are streamed. @@ -35,13 +41,11 @@ class Screen { } return _screenStateStream; } - throw ScreenStateException( - 'Screen State API exclusively available on Android!'); + throw ScreenStateException('Screen State API only available on Android'); } ScreenStateEvent _parseScreenStateEvent(String event) { switch (event) { - /** Android **/ case 'android.intent.action.SCREEN_OFF': return ScreenStateEvent.SCREEN_OFF; case 'android.intent.action.SCREEN_ON': diff --git a/packages/screen_state/pubspec.yaml b/packages/screen_state/pubspec.yaml index 699d3ca22..0bd472dfd 100644 --- a/packages/screen_state/pubspec.yaml +++ b/packages/screen_state/pubspec.yaml @@ -1,12 +1,11 @@ name: screen_state description: A plugin for reporting screen events while the flutter application is running in background. Works for Android only. -version: 2.0.0 +version: 3.0.1 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/screen_state -#author: CACHET Team environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.10.0" + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -16,15 +15,7 @@ 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: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -32,34 +23,3 @@ flutter: pluginClass: ScreenStatePlugin ios: pluginClass: ScreenStatePlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, 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 in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/screen_state/test/screen_state_test.dart b/packages/screen_state/test/screen_state_test.dart deleted file mode 100644 index 3e8b03bef..000000000 --- a/packages/screen_state/test/screen_state_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - const MethodChannel channel = MethodChannel('screen_state'); - - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); - }); - - tearDown(() { - channel.setMockMethodCallHandler(null); - }); - -// test('getPlatformVersion', () async { -// expect(await ScreenState.platformVersion, '42'); -// }); -} diff --git a/packages/weather/CHANGELOG.md b/packages/weather/CHANGELOG.md index 0965abfbf..c01392682 100644 --- a/packages/weather/CHANGELOG.md +++ b/packages/weather/CHANGELOG.md @@ -1,3 +1,16 @@ +## 3.1.1 + +- Fixed formatting issue + +## 3.1.0 + +- Updated http package to ^1.1.0 + +## 3.0.0 + +- Updates Kotlin plugin and AGP +- Upgrade of compileSdkVersion + ## 2.0.1 - Update Weather parsing to accept int values for double fields. diff --git a/packages/weather/example/android/app/build.gradle b/packages/weather/example/android/app/build.gradle index 0c9e75507..6daa735d0 100644 --- a/packages/weather/example/android/app/build.gradle +++ b/packages/weather/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion flutter.compileSdkVersion lintOptions { disable 'InvalidPackage' @@ -34,8 +34,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "cachet.plugins.weatherexample" - minSdkVersion 16 - targetSdkVersion 27 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/packages/weather/example/android/app/src/main/AndroidManifest.xml b/packages/weather/example/android/app/src/main/AndroidManifest.xml index 376564ca6..a3c970db6 100644 --- a/packages/weather/example/android/app/src/main/AndroidManifest.xml +++ b/packages/weather/example/android/app/src/main/AndroidManifest.xml @@ -14,7 +14,9 @@ android:launchMode="singleTop" android:name=".MainActivity" android:theme="@style/LaunchTheme" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true" +>