Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/health/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 12.2.0

* iOS: Add `deviceModel` in returned Health data to identify the device that generated the data of the receiver. (in iOS `source_name` represents the revision of the source responsible for saving the receiver.)
* Android: Add read health data in background - PR [#1184](https://github.com/cph-cachet/flutter-plugins/pull/1184)
* Fix [#1169](https://github.com/cph-cachet/flutter-plugins/issues/1169) where `meal_type` property in `Nutrition` was null always
* iOS: Add `CARDIO_DANCE` HealthDataType - [#1146](https://github.com/cph-cachet/flutter-plugins/pull/1146)

## 12.1.0

* Add delete record by UUID method. See function `deleteByUUID(required String uuid, HealthDataType? type)`
Expand Down
11 changes: 11 additions & 0 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ List<HealthDataPoint> points = ...;
points = health.removeDuplicates(points);
```

### Android: Reading Health Data in Background
Currently health connect allows apps to read health data in the background. In order to achieve this add the following permission to your `AndroidManifest.XML`:
```XML
<!-- For reading data in background -->
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>
```
Furthermore, the plugin now exposes three new functions to help you check and request access to read data in the background:
1. `isHealthDataInBackgroundAvailable()`: Checks if the Health Data in Background feature is available
2. `isHealthDataInBackgroundAuthorized()`: Checks the current status of the Health Data in Background permission
3. `requestHealthDataInBackgroundAuthorization()`: Requests the Health Data in Background permission.

## Data Types

The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
import androidx.health.connect.client.records.*
import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST
import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER
Expand Down Expand Up @@ -153,6 +154,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
"isHealthDataHistoryAvailable" -> isHealthDataHistoryAvailable(call, result)
"isHealthDataHistoryAuthorized" -> isHealthDataHistoryAuthorized(call, result)
"requestHealthDataHistoryAuthorization" -> requestHealthDataHistoryAuthorization(call, result)
"isHealthDataInBackgroundAvailable" -> isHealthDataInBackgroundAvailable(call, result)
"isHealthDataInBackgroundAuthorized" -> isHealthDataInBackgroundAuthorized(call, result)
"requestHealthDataInBackgroundAuthorization" -> requestHealthDataInBackgroundAuthorization(call, result)
"hasPermissions" -> hasPermissions(call, result)
"requestAuthorization" -> requestAuthorization(call, result)
"revokePermissions" -> revokePermissions(call, result)
Expand Down Expand Up @@ -568,6 +572,55 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
healthConnectRequestPermissionsLauncher!!.launch(setOf(PERMISSION_READ_HEALTH_DATA_HISTORY))
}

/**
* Checks if the health data in background feature is available on this device
*/
@OptIn(ExperimentalFeatureAvailabilityApi::class)
private fun isHealthDataInBackgroundAvailable(call: MethodCall, result: Result) {
scope.launch {
result.success(
healthConnectClient
.features
.getFeatureStatus(HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND) ==
HealthConnectFeatures.FEATURE_STATUS_AVAILABLE)
}
}

/**
* Checks if PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND has been granted
*/
private fun isHealthDataInBackgroundAuthorized(call: MethodCall, result: Result) {
scope.launch {
result.success(
healthConnectClient
.permissionController
.getGrantedPermissions()
.containsAll(listOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)),
)
}
}

/**
* Requests authorization for PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
*/
private fun requestHealthDataInBackgroundAuthorization(call: MethodCall, result: Result) {
if (context == null) {
result.success(false)
return
}

if (healthConnectRequestPermissionsLauncher == null) {
result.success(false)
Log.i("FLUTTER_HEALTH", "Permission launcher not found")
return
}

// Store the result to be called in [onHealthConnectPermissionCallback]
mResult = result
isReplySubmitted = false
healthConnectRequestPermissionsLauncher!!.launch(setOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND))
}

private fun hasPermissions(call: MethodCall, result: Result) {
val args = call.arguments as HashMap<*, *>
val types = (args["types"] as? ArrayList<*>)?.filterIsInstance<String>()!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
<!-- For reading historical data - more than 30 days ago since permission given -->
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY"/>

<!-- For reading data in background -->
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>

<application
android:label="health_example"
android:name="${applicationName}"
Expand Down
5 changes: 4 additions & 1 deletion packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ class HealthAppState extends State<HealthApp> {
// request access to read historic data
await health.requestHealthDataHistoryAuthorization();

// request access in background
await health.requestHealthDataInBackgroundAuthorization();

} catch (error) {
debugPrint("Exception in authorize: $error");
}
Expand Down Expand Up @@ -737,7 +740,7 @@ class HealthAppState extends State<HealthApp> {
if (p.value is NutritionHealthValue) {
return ListTile(
title: Text(
"${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"),
"${p.typeString} ${(p.value as NutritionHealthValue).meal_type}: ${(p.value as NutritionHealthValue).name}"),
trailing:
Text('${(p.value as NutritionHealthValue).calories} kcal'),
subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'),
Expand Down
1 change: 1 addition & 0 deletions packages/health/ios/Classes/SwiftHealthPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
"date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
"source_id": sample.sourceRevision.source.bundleIdentifier,
"source_name": sample.sourceRevision.source.name,
"device_model": sample.device?.model ?? "unknown",
"recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
? RecordingMethod.manual.rawValue
: RecordingMethod.automatic.rawValue,
Expand Down
2 changes: 1 addition & 1 deletion packages/health/ios/health.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
Pod::Spec.new do |s|
s.name = 'health'
s.version = '12.1.0'
s.version = '12.2.0'
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
s.description = <<-DESC
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
Expand Down
6 changes: 4 additions & 2 deletions packages/health/lib/health.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions packages/health/lib/src/health_data_point.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class HealthDataPoint {
/// The metadata for this data point.
Map<String, dynamic>? metadata;

/// The source of the data, whether from the iPhone or Watch or something else.
/// Only available fo iOS
/// On Android: always return null
String? deviceModel;

HealthDataPoint({
required this.uuid,
required this.value,
Expand All @@ -69,6 +74,7 @@ class HealthDataPoint {
this.recordingMethod = RecordingMethod.unknown,
this.workoutSummary,
this.metadata,
this.deviceModel,
}) {
// set the value to minutes rather than the category
// returned by the native API
Expand Down Expand Up @@ -137,6 +143,7 @@ class HealthDataPoint {
: Map<String, dynamic>.from(dataPoint['metadata'] as Map);
final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT;
final String? uuid = dataPoint["uuid"] as String?;
final String? deviceModel = dataPoint["device_model"] as String?;

// Set WorkoutSummary, if available.
WorkoutSummary? workoutSummary;
Expand All @@ -163,6 +170,7 @@ class HealthDataPoint {
recordingMethod: RecordingMethod.fromInt(recordingMethod),
workoutSummary: workoutSummary,
metadata: metadata,
deviceModel: deviceModel,
);
}

Expand All @@ -180,7 +188,8 @@ class HealthDataPoint {
sourceName: $sourceName
recordingMethod: $recordingMethod
workoutSummary: $workoutSummary
metadata: $metadata""";
metadata: $metadata
deviceModel: $deviceModel""";

@override
bool operator ==(Object other) =>
Expand All @@ -196,9 +205,10 @@ class HealthDataPoint {
sourceId == other.sourceId &&
sourceName == other.sourceName &&
recordingMethod == other.recordingMethod &&
metadata == other.metadata;
metadata == other.metadata &&
deviceModel == other.deviceModel;

@override
int get hashCode => Object.hash(uuid, value, unit, dateFrom, dateTo, type,
sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata);
sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata, deviceModel);
}
64 changes: 64 additions & 0 deletions packages/health/lib/src/health_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,70 @@ class Health {
}
}

/// Checks if the Health Data in Background feature is available.
///
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
///
///
/// Android only. Returns false on iOS or if an error occurs.
Future<bool> isHealthDataInBackgroundAvailable() async {
if (Platform.isIOS) return false;

try {
final status =
await _channel.invokeMethod<bool>('isHealthDataInBackgroundAvailable');
return status ?? false;
} catch (e) {
debugPrint(
'$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e');
return false;
}
}

/// Checks the current status of the Health Data in Background permission.
/// Make sure to check [isHealthConnectAvailable] before calling this method.
///
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
///
///
/// Android only. Returns true on iOS or false if an error occurs.
Future<bool> isHealthDataInBackgroundAuthorized() async {
if (Platform.isIOS) return true;

try {
final status =
await _channel.invokeMethod<bool>('isHealthDataInBackgroundAuthorized');
return status ?? false;
} catch (e) {
debugPrint(
'$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e');
return false;
}
}

/// Requests the Health Data in Background permission.
///
/// Returns true if successful, false otherwise.
///
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
///
///
/// Android only. Returns true on iOS or false if an error occurs.
Future<bool> requestHealthDataInBackgroundAuthorization() async {
if (Platform.isIOS) return true;

await _checkIfHealthConnectAvailableOnAndroid();
try {
final bool? isAuthorized =
await _channel.invokeMethod('requestHealthDataInBackgroundAuthorization');
return isAuthorized ?? false;
} catch (e) {
debugPrint(
'$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e');
return false;
}
}

/// Requests permissions to access health data [types].
///
/// Returns true if successful, false otherwise.
Expand Down
8 changes: 4 additions & 4 deletions packages/health/lib/src/health_value_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ class NutritionHealthValue extends HealthValue {
String? name;

/// The type of meal.
String? mealType;
String? meal_type;

/// The amount of calories in kcal.
double? calories;
Expand Down Expand Up @@ -556,7 +556,7 @@ class NutritionHealthValue extends HealthValue {

NutritionHealthValue({
this.name,
this.mealType,
this.meal_type,
this.calories,
this.protein,
this.fat,
Expand Down Expand Up @@ -625,7 +625,7 @@ class NutritionHealthValue extends HealthValue {
name: ${name.toString()},
carbs: ${carbs.toString()},
caffeine: ${caffeine.toString()},
mealType: $mealType,
mealType: $meal_type,
vitaminA: ${vitaminA.toString()},
b1Thiamine: ${b1Thiamine.toString()},
b2Riboflavin: ${b2Riboflavin.toString()},
Expand Down Expand Up @@ -668,7 +668,7 @@ class NutritionHealthValue extends HealthValue {
bool operator ==(Object other) =>
other is NutritionHealthValue &&
other.name == name &&
other.mealType == mealType &&
other.meal_type == meal_type &&
other.calories == calories &&
other.protein == protein &&
other.fat == fat &&
Expand Down
2 changes: 1 addition & 1 deletion packages/health/lib/src/heath_data_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ enum HealthWorkoutActivityType {
BASKETBALL,
BIKING, // This also entails the iOS version where it is called CYCLING
BOXING,
CARDIO_DANCE,
CRICKET,
CROSS_COUNTRY_SKIING,
CURLING,
Expand Down Expand Up @@ -508,7 +509,6 @@ enum HealthWorkoutActivityType {
// iOS only
BARRE,
BOWLING,
CARDIO_DANCE,
CLIMBING,
COOLDOWN,
CORE_TRAINING,
Expand Down
2 changes: 1 addition & 1 deletion packages/health/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: health
description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
version: 12.1.0
version: 12.2.0
homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health

environment:
Expand Down