Skip to content

Commit

Permalink
feat: Add method to open native health settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc Sanny committed Sep 1, 2023
1 parent 5cc3b59 commit 114c731
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
when (call.method) {
"useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result)
"checkAvailability" -> checkAvailability(call, result)
"openSystemSettings" -> openSystemSettings(call, result)
"hasPermissions" -> hasPermissions(call, result)
"requestAuthorization" -> requestAuthorization(call, result)
"revokePermissions" -> revokePermissions(call, result)
Expand Down Expand Up @@ -1442,6 +1443,18 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
result?.success(healthConnectAvailable)
}

fun openSystemSettings(call: MethodCall, result: Result) {
try {
val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.action = HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS
context?.startActivity(intent)
result.success(true)
} catch (e: Throwable) {
result.error("UNABLE_TO_START_ACTIVITY", e.message, e)
}
}

fun useHealthConnectIfAvailable(call: MethodCall, result: Result) {
useHealthConnectIfAvailable = true
result.success(null)
Expand Down
8 changes: 8 additions & 0 deletions packages/health/ios/Classes/SwiftHealthPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
else if call.method.elementsEqual("requestAuthorization") {
try! requestAuthorization(call: call, result: result)
}

else if call.method.elementsEqual("openSystemSettings") {
openSystemSettings(call: call, result: result)
}

/// Handle getData
else if call.method.elementsEqual("getData") {
Expand Down Expand Up @@ -182,6 +186,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) {
result(HKHealthStore.isHealthDataAvailable())
}

func openSystemSettings(call: FlutterMethodCall, result: @escaping FlutterResult) {
UIApplication.shared.open(URL(string: "x-apple-health://")!)
}

func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
let arguments = call.arguments as? NSDictionary
Expand Down
110 changes: 42 additions & 68 deletions packages/health/lib/src/health_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,20 @@ class HealthFactory {
final _deviceInfo = DeviceInfoPlugin();
late bool _useHealthConnectIfAvailable;

static PlatformType _platformType =
Platform.isAndroid ? PlatformType.ANDROID : PlatformType.IOS;
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');
if (_useHealthConnectIfAvailable) _channel.invokeMethod('useHealthConnectIfAvailable');
}

/// Check if a given data type is available on the platform
bool isDataTypeAvailable(HealthDataType dataType) =>
_platformType == PlatformType.ANDROID
? _dataTypeKeysAndroid.contains(dataType)
: _dataTypeKeysIOS.contains(dataType);
bool isDataTypeAvailable(HealthDataType dataType) => _platformType == PlatformType.ANDROID
? _dataTypeKeysAndroid.contains(dataType)
: _dataTypeKeysIOS.contains(dataType);

/// Determines if the data types have been granted with the specified access rights.
///
Expand Down Expand Up @@ -60,13 +57,11 @@ class HealthFactory {
Future<bool?> hasPermissions(List<HealthDataType> types,
{List<HealthDataAccess>? permissions}) async {
if (permissions != null && permissions.length != types.length)
throw ArgumentError(
"The lists of types and permissions must be of same length.");
throw ArgumentError("The lists of types and permissions must be of same length.");

final mTypes = List<HealthDataType>.from(types, growable: true);
final mPermissions = permissions == null
? List<int>.filled(types.length, HealthDataAccess.READ.index,
growable: true)
? List<int>.filled(types.length, HealthDataAccess.READ.index, growable: true)
: permissions.map((permission) => permission.index).toList();

/// On Android, if BMI is requested, then also ask for weight and height
Expand Down Expand Up @@ -107,6 +102,15 @@ class HealthFactory {
return result ?? false;
}

/// Opens native system settings for:
/// - Health on iOS
/// - Health Connect on Android
///
/// Throws if the application is not installed.
Future<void> openSystemSettings() async {
await _channel.invokeMethod('openSystemSettings');
}

/// Requests permissions to access data types in Apple Health or Google Fit.
///
/// Returns true if successful, false otherwise
Expand All @@ -129,8 +133,7 @@ class HealthFactory {
List<HealthDataAccess>? permissions,
}) async {
if (permissions != null && permissions.length != types.length) {
throw ArgumentError(
'The length of [types] must be same as that of [permissions].');
throw ArgumentError('The length of [types] must be same as that of [permissions].');
}

if (permissions != null) {
Expand All @@ -151,16 +154,15 @@ class HealthFactory {

final mTypes = List<HealthDataType>.from(types, growable: true);
final mPermissions = permissions == null
? List<int>.filled(types.length, HealthDataAccess.READ.index,
growable: true)
? List<int>.filled(types.length, HealthDataAccess.READ.index, growable: true)
: permissions.map((permission) => permission.index).toList();

// on Android, if BMI is requested, then also ask for weight and height
if (_platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions);

List<String> keys = mTypes.map((e) => e.name).toList();
final bool? isAuthorized = await _channel.invokeMethod(
'requestAuthorization', {'types': keys, "permissions": mPermissions});
final bool? isAuthorized = await _channel
.invokeMethod('requestAuthorization', {'types': keys, "permissions": mPermissions});
return isAuthorized ?? false;
}

Expand All @@ -183,39 +185,25 @@ class HealthFactory {
}

/// Calculate the BMI using the last observed height and weight values.
Future<List<HealthDataPoint>> _computeAndroidBMI(
DateTime startTime, DateTime endTime) async {
List<HealthDataPoint> heights =
await _prepareQuery(startTime, endTime, HealthDataType.HEIGHT);
Future<List<HealthDataPoint>> _computeAndroidBMI(DateTime startTime, DateTime endTime) async {
List<HealthDataPoint> heights = await _prepareQuery(startTime, endTime, HealthDataType.HEIGHT);

if (heights.isEmpty) {
return [];
}

List<HealthDataPoint> weights =
await _prepareQuery(startTime, endTime, HealthDataType.WEIGHT);
List<HealthDataPoint> weights = await _prepareQuery(startTime, endTime, HealthDataType.WEIGHT);

double h =
(heights.last.value as NumericHealthValue).numericValue.toDouble();
double h = (heights.last.value as NumericHealthValue).numericValue.toDouble();

const dataType = HealthDataType.BODY_MASS_INDEX;
final unit = _dataTypeToUnit[dataType]!;

final bmiHealthPoints = <HealthDataPoint>[];
for (var i = 0; i < weights.length; i++) {
final bmiValue =
(weights[i].value as NumericHealthValue).numericValue.toDouble() /
(h * h);
final x = HealthDataPoint(
NumericHealthValue(bmiValue),
dataType,
unit,
weights[i].dateFrom,
weights[i].dateTo,
_platformType,
_deviceId!,
'',
'');
final bmiValue = (weights[i].value as NumericHealthValue).numericValue.toDouble() / (h * h);
final x = HealthDataPoint(NumericHealthValue(bmiValue), dataType, unit, weights[i].dateFrom,
weights[i].dateTo, _platformType, _deviceId!, '', '');

bmiHealthPoints.add(x);
}
Expand Down Expand Up @@ -245,8 +233,7 @@ class HealthFactory {
HealthDataUnit? unit,
}) async {
if (type == HealthDataType.WORKOUT)
throw ArgumentError(
"Adding workouts should be done using the writeWorkoutData method.");
throw ArgumentError("Adding workouts should be done using the writeWorkoutData method.");
if (startTime.isAfter(endTime))
throw ArgumentError("startTime must be equal or earlier than endTime");
if ({
Expand All @@ -256,8 +243,7 @@ class HealthFactory {
HealthDataType.ELECTROCARDIOGRAM,
}.contains(type) &&
_platformType == PlatformType.IOS)
throw ArgumentError(
"$type - iOS doesnt support writing this data type in HealthKit");
throw ArgumentError("$type - iOS doesnt support writing this data type in HealthKit");

// Assign default unit if not specified
unit ??= _dataTypeToUnit[type]!;
Expand Down Expand Up @@ -296,8 +282,7 @@ class HealthFactory {
/// + 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].
Future<bool> delete(
HealthDataType type, DateTime startTime, DateTime endTime) async {
Future<bool> delete(HealthDataType type, DateTime startTime, DateTime endTime) async {
if (startTime.isAfter(endTime))
throw ArgumentError("startTime must be equal or earlier than endTime");

Expand Down Expand Up @@ -349,16 +334,14 @@ 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 blood oxygen saturation is measured only at a specific point in time.
Future<bool> writeBloodOxygen(
double saturation, DateTime startTime, DateTime endTime,
Future<bool> 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);
success = await writeHealthData(saturation, HealthDataType.BLOOD_OXYGEN, startTime, endTime);
} else if (_platformType == PlatformType.ANDROID) {
Map<String, dynamic> args = {
'value': saturation,
Expand Down Expand Up @@ -386,16 +369,10 @@ class HealthFactory {
/// + It must be equal to or later than [startTime].
/// + Simply set [endTime] equal to [startTime] if the audiogram is measured only at a specific point in time.
/// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID and HKMetadataKeyDeviceName are required
Future<bool> writeAudiogram(
List<double> frequencies,
List<double> leftEarSensitivities,
List<double> rightEarSensitivities,
DateTime startTime,
DateTime endTime,
Future<bool> writeAudiogram(List<double> frequencies, List<double> leftEarSensitivities,
List<double> rightEarSensitivities, DateTime startTime, DateTime endTime,
{Map<String, dynamic>? metadata}) async {
if (frequencies.isEmpty ||
leftEarSensitivities.isEmpty ||
rightEarSensitivities.isEmpty)
if (frequencies.isEmpty || leftEarSensitivities.isEmpty || rightEarSensitivities.isEmpty)
throw ArgumentError(
"frequencies, leftEarSensitivities and rightEarSensitivities can't be empty");
if (frequencies.length != leftEarSensitivities.length ||
Expand Down Expand Up @@ -447,13 +424,11 @@ class HealthFactory {

// If not implemented on platform, throw an exception
if (!isDataTypeAvailable(dataType)) {
throw HealthException(
dataType, 'Not available on platform $_platformType');
throw HealthException(dataType, 'Not available on platform $_platformType');
}

// If BodyMassIndex is requested on Android, calculate this manually
if (dataType == HealthDataType.BODY_MASS_INDEX &&
_platformType == PlatformType.ANDROID) {
if (dataType == HealthDataType.BODY_MASS_INDEX && _platformType == PlatformType.ANDROID) {
return _computeAndroidBMI(startTime, endTime);
}
return await _dataQuery(startTime, endTime, dataType);
Expand Down Expand Up @@ -603,12 +578,11 @@ class HealthFactory {
}) async {
// Check that value is on the current Platform
if (_platformType == PlatformType.IOS && !_isOnIOS(activityType)) {
throw HealthException(activityType,
"Workout activity type $activityType is not supported on iOS");
} else if (_platformType == PlatformType.ANDROID &&
!_isOnAndroid(activityType)) {
throw HealthException(activityType,
"Workout activity type $activityType is not supported on Android");
throw HealthException(
activityType, "Workout activity type $activityType is not supported on iOS");
} else if (_platformType == PlatformType.ANDROID && !_isOnAndroid(activityType)) {
throw HealthException(
activityType, "Workout activity type $activityType is not supported on Android");
}
final args = <String, dynamic>{
'activityType': activityType.name,
Expand Down

0 comments on commit 114c731

Please sign in to comment.