Skip to content

Commit ecd2ba5

Browse files
authored
Merge pull request #1192 from cph-cachet/iarata/health-12
Health 12.2.0
2 parents f3f9ab7 + cb2e3b8 commit ecd2ba5

File tree

13 files changed

+167
-13
lines changed

13 files changed

+167
-13
lines changed

packages/health/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 12.2.0
2+
3+
* 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.)
4+
* Android: Add read health data in background - PR [#1184](https://github.com/cph-cachet/flutter-plugins/pull/1184)
5+
* Fix [#1169](https://github.com/cph-cachet/flutter-plugins/issues/1169) where `meal_type` property in `Nutrition` was null always
6+
* iOS: Add `CARDIO_DANCE` HealthDataType - [#1146](https://github.com/cph-cachet/flutter-plugins/pull/1146)
7+
18
## 12.1.0
29

310
* Add delete record by UUID method. See function `deleteByUUID(required String uuid, HealthDataType? type)`

packages/health/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ List<HealthDataPoint> points = ...;
311311
points = health.removeDuplicates(points);
312312
```
313313

314+
### Android: Reading Health Data in Background
315+
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`:
316+
```XML
317+
<!-- For reading data in background -->
318+
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>
319+
```
320+
Furthermore, the plugin now exposes three new functions to help you check and request access to read data in the background:
321+
1. `isHealthDataInBackgroundAvailable()`: Checks if the Health Data in Background feature is available
322+
2. `isHealthDataInBackgroundAuthorized()`: Checks the current status of the Health Data in Background permission
323+
3. `requestHealthDataInBackgroundAuthorization()`: Requests the Health Data in Background permission.
324+
314325
## Data Types
315326

316327
The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html).

packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.health.connect.client.HealthConnectFeatures
1515
import androidx.health.connect.client.PermissionController
1616
import androidx.health.connect.client.permission.HealthPermission
1717
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY
18+
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
1819
import androidx.health.connect.client.records.*
1920
import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST
2021
import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER
@@ -153,6 +154,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
153154
"isHealthDataHistoryAvailable" -> isHealthDataHistoryAvailable(call, result)
154155
"isHealthDataHistoryAuthorized" -> isHealthDataHistoryAuthorized(call, result)
155156
"requestHealthDataHistoryAuthorization" -> requestHealthDataHistoryAuthorization(call, result)
157+
"isHealthDataInBackgroundAvailable" -> isHealthDataInBackgroundAvailable(call, result)
158+
"isHealthDataInBackgroundAuthorized" -> isHealthDataInBackgroundAuthorized(call, result)
159+
"requestHealthDataInBackgroundAuthorization" -> requestHealthDataInBackgroundAuthorization(call, result)
156160
"hasPermissions" -> hasPermissions(call, result)
157161
"requestAuthorization" -> requestAuthorization(call, result)
158162
"revokePermissions" -> revokePermissions(call, result)
@@ -568,6 +572,55 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
568572
healthConnectRequestPermissionsLauncher!!.launch(setOf(PERMISSION_READ_HEALTH_DATA_HISTORY))
569573
}
570574

575+
/**
576+
* Checks if the health data in background feature is available on this device
577+
*/
578+
@OptIn(ExperimentalFeatureAvailabilityApi::class)
579+
private fun isHealthDataInBackgroundAvailable(call: MethodCall, result: Result) {
580+
scope.launch {
581+
result.success(
582+
healthConnectClient
583+
.features
584+
.getFeatureStatus(HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND) ==
585+
HealthConnectFeatures.FEATURE_STATUS_AVAILABLE)
586+
}
587+
}
588+
589+
/**
590+
* Checks if PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND has been granted
591+
*/
592+
private fun isHealthDataInBackgroundAuthorized(call: MethodCall, result: Result) {
593+
scope.launch {
594+
result.success(
595+
healthConnectClient
596+
.permissionController
597+
.getGrantedPermissions()
598+
.containsAll(listOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)),
599+
)
600+
}
601+
}
602+
603+
/**
604+
* Requests authorization for PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
605+
*/
606+
private fun requestHealthDataInBackgroundAuthorization(call: MethodCall, result: Result) {
607+
if (context == null) {
608+
result.success(false)
609+
return
610+
}
611+
612+
if (healthConnectRequestPermissionsLauncher == null) {
613+
result.success(false)
614+
Log.i("FLUTTER_HEALTH", "Permission launcher not found")
615+
return
616+
}
617+
618+
// Store the result to be called in [onHealthConnectPermissionCallback]
619+
mResult = result
620+
isReplySubmitted = false
621+
healthConnectRequestPermissionsLauncher!!.launch(setOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND))
622+
}
623+
571624
private fun hasPermissions(call: MethodCall, result: Result) {
572625
val args = call.arguments as HashMap<*, *>
573626
val types = (args["types"] as? ArrayList<*>)?.filterIsInstance<String>()!!

packages/health/example/android/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
<!-- For reading historical data - more than 30 days ago since permission given -->
5858
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY"/>
5959

60+
<!-- For reading data in background -->
61+
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND"/>
62+
6063
<application
6164
android:label="health_example"
6265
android:name="${applicationName}"

packages/health/example/lib/main.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ class HealthAppState extends State<HealthApp> {
126126
// request access to read historic data
127127
await health.requestHealthDataHistoryAuthorization();
128128

129+
// request access in background
130+
await health.requestHealthDataInBackgroundAuthorization();
131+
129132
} catch (error) {
130133
debugPrint("Exception in authorize: $error");
131134
}
@@ -737,7 +740,7 @@ class HealthAppState extends State<HealthApp> {
737740
if (p.value is NutritionHealthValue) {
738741
return ListTile(
739742
title: Text(
740-
"${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"),
743+
"${p.typeString} ${(p.value as NutritionHealthValue).meal_type}: ${(p.value as NutritionHealthValue).name}"),
741744
trailing:
742745
Text('${(p.value as NutritionHealthValue).calories} kcal'),
743746
subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'),

packages/health/ios/Classes/SwiftHealthPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
902902
"date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
903903
"source_id": sample.sourceRevision.source.bundleIdentifier,
904904
"source_name": sample.sourceRevision.source.name,
905+
"device_model": sample.device?.model ?? "unknown",
905906
"recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
906907
? RecordingMethod.manual.rawValue
907908
: RecordingMethod.automatic.rawValue,

packages/health/ios/health.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
Pod::Spec.new do |s|
55
s.name = 'health'
6-
s.version = '12.1.0'
6+
s.version = '12.2.0'
77
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
88
s.description = <<-DESC
99
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.

packages/health/lib/health.g.dart

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/health/lib/src/health_data_point.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class HealthDataPoint {
5555
/// The metadata for this data point.
5656
Map<String, dynamic>? metadata;
5757

58+
/// The source of the data, whether from the iPhone or Watch or something else.
59+
/// Only available fo iOS
60+
/// On Android: always return null
61+
String? deviceModel;
62+
5863
HealthDataPoint({
5964
required this.uuid,
6065
required this.value,
@@ -69,6 +74,7 @@ class HealthDataPoint {
6974
this.recordingMethod = RecordingMethod.unknown,
7075
this.workoutSummary,
7176
this.metadata,
77+
this.deviceModel,
7278
}) {
7379
// set the value to minutes rather than the category
7480
// returned by the native API
@@ -137,6 +143,7 @@ class HealthDataPoint {
137143
: Map<String, dynamic>.from(dataPoint['metadata'] as Map);
138144
final unit = dataTypeToUnit[dataType] ?? HealthDataUnit.UNKNOWN_UNIT;
139145
final String? uuid = dataPoint["uuid"] as String?;
146+
final String? deviceModel = dataPoint["device_model"] as String?;
140147

141148
// Set WorkoutSummary, if available.
142149
WorkoutSummary? workoutSummary;
@@ -163,6 +170,7 @@ class HealthDataPoint {
163170
recordingMethod: RecordingMethod.fromInt(recordingMethod),
164171
workoutSummary: workoutSummary,
165172
metadata: metadata,
173+
deviceModel: deviceModel,
166174
);
167175
}
168176

@@ -180,7 +188,8 @@ class HealthDataPoint {
180188
sourceName: $sourceName
181189
recordingMethod: $recordingMethod
182190
workoutSummary: $workoutSummary
183-
metadata: $metadata""";
191+
metadata: $metadata
192+
deviceModel: $deviceModel""";
184193

185194
@override
186195
bool operator ==(Object other) =>
@@ -196,9 +205,10 @@ class HealthDataPoint {
196205
sourceId == other.sourceId &&
197206
sourceName == other.sourceName &&
198207
recordingMethod == other.recordingMethod &&
199-
metadata == other.metadata;
208+
metadata == other.metadata &&
209+
deviceModel == other.deviceModel;
200210

201211
@override
202212
int get hashCode => Object.hash(uuid, value, unit, dateFrom, dateTo, type,
203-
sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata);
213+
sourcePlatform, sourceDeviceId, sourceId, sourceName, metadata, deviceModel);
204214
}

packages/health/lib/src/health_plugin.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,70 @@ class Health {
264264
}
265265
}
266266

267+
/// Checks if the Health Data in Background feature is available.
268+
///
269+
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
270+
///
271+
///
272+
/// Android only. Returns false on iOS or if an error occurs.
273+
Future<bool> isHealthDataInBackgroundAvailable() async {
274+
if (Platform.isIOS) return false;
275+
276+
try {
277+
final status =
278+
await _channel.invokeMethod<bool>('isHealthDataInBackgroundAvailable');
279+
return status ?? false;
280+
} catch (e) {
281+
debugPrint(
282+
'$runtimeType - Exception in isHealthDataInBackgroundAvailable(): $e');
283+
return false;
284+
}
285+
}
286+
287+
/// Checks the current status of the Health Data in Background permission.
288+
/// Make sure to check [isHealthConnectAvailable] before calling this method.
289+
///
290+
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
291+
///
292+
///
293+
/// Android only. Returns true on iOS or false if an error occurs.
294+
Future<bool> isHealthDataInBackgroundAuthorized() async {
295+
if (Platform.isIOS) return true;
296+
297+
try {
298+
final status =
299+
await _channel.invokeMethod<bool>('isHealthDataInBackgroundAuthorized');
300+
return status ?? false;
301+
} catch (e) {
302+
debugPrint(
303+
'$runtimeType - Exception in isHealthDataInBackgroundAuthorized(): $e');
304+
return false;
305+
}
306+
}
307+
308+
/// Requests the Health Data in Background permission.
309+
///
310+
/// Returns true if successful, false otherwise.
311+
///
312+
/// See this for more info: https://developer.android.com/reference/androidx/health/connect/client/permission/HealthPermission#PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND()
313+
///
314+
///
315+
/// Android only. Returns true on iOS or false if an error occurs.
316+
Future<bool> requestHealthDataInBackgroundAuthorization() async {
317+
if (Platform.isIOS) return true;
318+
319+
await _checkIfHealthConnectAvailableOnAndroid();
320+
try {
321+
final bool? isAuthorized =
322+
await _channel.invokeMethod('requestHealthDataInBackgroundAuthorization');
323+
return isAuthorized ?? false;
324+
} catch (e) {
325+
debugPrint(
326+
'$runtimeType - Exception in requestHealthDataInBackgroundAuthorization(): $e');
327+
return false;
328+
}
329+
}
330+
267331
/// Requests permissions to access health data [types].
268332
///
269333
/// Returns true if successful, false otherwise.

0 commit comments

Comments
 (0)