Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix of permission in example app + improvements to doc #875

Merged
merged 3 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
53 changes: 35 additions & 18 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Note that for Android, the target phone **needs** to have [Google Fit](https://w

### Apple Health (iOS)

Step 1: Append the Info.plist with the following 2 entries
Step 1: Append the `Info.plist` with the following 2 entries

```xml
<key>NSHealthShareUsageDescription</key>
Expand All @@ -89,7 +89,7 @@ Step 2: Open your Flutter project in Xcode by right clicking on the "ios" folder

### Google Fit (Android option 1)

Follow the guide at https://developers.google.com/fit/android/get-api-key
Follow the guide at <https://developers.google.com/fit/android/get-api-key>

Below is an example of following the guide:

Expand Down Expand Up @@ -118,61 +118,78 @@ Certificate fingerprints:
Version: 3
```

Follow the instructions at https://developers.google.com/fit/android/get-api-key for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential.
Follow the instructions at <https://developers.google.com/fit/android/get-api-key> for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential.

The client id will look something like `YOUR_CLIENT_ID.apps.googleusercontent.com`.

### Health Connect (Android option 2)

Health Connect requires the following lines in the `AndroidManifest.xml` file (also seen in the example app):

```
```xml
<!-- Check whether Health Connect is installed or not -->
<queries>
<package android:name="com.google.android.apps.healthdata" />
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
<package android:name="com.google.android.apps.healthdata" />
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
</queries>
```

### Android Permissions

Starting from API level 28 (Android 9.0) acessing some fitness data (e.g. Steps) requires a special permission.
Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission.

To set it add the following line to your `AndroidManifest.xml` file.

```
```xml
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
```

#### Health Connect

If using Health Connect on Android it requires speciel permissions in the `AndroidManifest.xml` file.
The permissions can be found here: https://developer.android.com/guide/health-and-fitness/health-connect/data-and-data-types/data-types
If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: <https://developer.android.com/guide/health-and-fitness/health-connect/data-and-data-types/data-types>

Example shown here (can also be found in the example app):

```
```xml
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
...
```

Furthermore, an `intent-filter` needs to be added to the `.MainActivity` activity.

```xml
<activity
android:name=".MainActivity"
android:exported="true"

....

<!-- Intention to show Permissions screen for Health Connect API -->
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
```

#### Workout permissions

Additionally, for Workouts: If the distance of a workout is requested then the location permissions below are needed.

```
```xml
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
```

There's a `debug`, `main` and `profile` version which are chosen depending on how you start your app. In general, it's sufficient to add permission only to the `main` version.

Beacuse this is labled as a `dangerous` protection level, the permission system will not grant it automaticlly and it requires the user's action.
Because this is labeled as a `dangerous` protection level, the permission system will not grant it automatically and it requires the user's action.
You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin.
Follow the plugin setup instructions and add the following line before requsting the data:
Follow the plugin setup instructions and add the following line before requesting the data:

```
```dart
await Permission.activityRecognition.request();
await Permission.location.request();
```
Expand Down Expand Up @@ -256,7 +273,7 @@ NB for iOS: The device must be unlocked before Health data can be requested, oth

```
flutter: Health Plugin Error:
flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible}))
flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible}))
```

### Filtering out duplicates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
</queries>

<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
Expand Down Expand Up @@ -76,6 +77,8 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

<!-- Intention to show Permissions screen for Health Connect API -->
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
Expand Down
36 changes: 23 additions & 13 deletions packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class _HealthAppState extends State<HealthApp> {
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.

// Use the entire list on e.g. Android.
static final types = dataTypesAndroid;
// Or selected types

// Or specify specific types
// static final types = [
// HealthDataType.WEIGHT,
// HealthDataType.STEPS,
Expand All @@ -45,19 +45,22 @@ class _HealthAppState extends State<HealthApp> {
// HealthDataType.WORKOUT,
// HealthDataType.BLOOD_PRESSURE_DIASTOLIC,
// HealthDataType.BLOOD_PRESSURE_SYSTOLIC,
// // Uncomment these lines on iOS - only available on iOS
// // Uncomment this line on iOS - only available on iOS
// // HealthDataType.AUDIOGRAM
// ];

// with corresponsing permissions
// Set up corresponding permissions

// READ only
// final permissions = types.map((e) => HealthDataAccess.READ).toList();
// Or READ and WRITE

// Or both READ and WRITE
final permissions = types.map((e) => HealthDataAccess.READ_WRITE).toList();

// create a HealthFactory for use in the app
HealthFactory health = HealthFactory(useHealthConnectIfAvailable: true);

/// Authorize, i.e. get permissions to access relevant health data.
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.
Expand All @@ -67,7 +70,7 @@ class _HealthAppState extends State<HealthApp> {
await Permission.activityRecognition.request();
await Permission.location.request();

// Check if we have permission
// Check if we have health permissions
bool? hasPermissions =
await health.hasPermissions(types, permissions: permissions);

Expand Down Expand Up @@ -217,9 +220,14 @@ class _HealthAppState extends State<HealthApp> {
final now = DateTime.now();
final midnight = DateTime(now.year, now.month, now.day);

bool requested = await health.requestAuthorization([HealthDataType.STEPS]);
bool stepsPermission =
await health.hasPermissions([HealthDataType.STEPS]) ?? false;
if (!stepsPermission) {
stepsPermission =
await health.requestAuthorization([HealthDataType.STEPS]);
}

if (requested) {
if (stepsPermission) {
try {
steps = await health.getTotalStepsInInterval(midnight, now);
} catch (error) {
Expand All @@ -238,6 +246,7 @@ class _HealthAppState extends State<HealthApp> {
}
}

/// Revoke access to health data. Note, this only has an effect on Android.
Future revokeAccess() async {
try {
await health.revokePermissions();
Expand Down Expand Up @@ -305,9 +314,10 @@ class _HealthAppState extends State<HealthApp> {
Widget _contentNotFetched() {
return Column(
children: [
Text('Press the download button to fetch data.'),
Text('Press the plus button to insert some random data.'),
Text('Press the walking button to get total step count.'),
Text("Press 'Auth' to get permissions to access health data."),
Text("Press 'Fetch Dat' to get health data."),
Text("Press 'Add Data' to add some random health data."),
Text("Press 'Delete Data' to remove some random health data."),
],
mainAxisAlignment: MainAxisAlignment.center,
);
Expand Down
6 changes: 5 additions & 1 deletion packages/health/example/lib/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ const List<HealthDataType> dataTypesIOS = [
HealthDataType.NUTRITION,
];

/// List of data types available on Android
/// List of data types available on Android.
///
/// Note that these are only the ones supported on Android's Health Connect API.
/// Android's Google Fit have more types that we support in the [HealthDataType]
/// enumeration.
const List<HealthDataType> dataTypesAndroid = [
HealthDataType.ACTIVE_ENERGY_BURNED,
HealthDataType.BASAL_ENERGY_BURNED,
Expand Down
14 changes: 10 additions & 4 deletions packages/health/lib/src/health_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,14 @@ class HealthFactory {
/// + If specified, each [HealthDataAccess] in this list is requested for its corresponding indexed
/// entry in [types]. In addition, the length of this list must be equal to that of [types].
///
/// Caveat:
/// Caveats:
///
/// As Apple HealthKit will not disclose if READ access has been granted for a data type due to privacy concern,
/// this method will return **true if the window asking for permission was showed to the user without errors**
/// if it is called on iOS with a READ or READ_WRITE access.
/// * This method may block if permissions are already granted. Hence, check
/// [hasPermissions] before calling this method.
/// * As Apple HealthKit will not disclose if READ access has been granted for
/// a data type due to privacy concern, this method will return **true if
/// the window asking for permission was showed to the user without errors**
/// if it is called on iOS with a READ or READ_WRITE access.
Future<bool> requestAuthorization(
List<HealthDataType> types, {
List<HealthDataAccess>? permissions,
Expand Down Expand Up @@ -147,8 +150,11 @@ class HealthFactory {
if (_platformType == PlatformType.ANDROID) _handleBMI(mTypes, mPermissions);

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

Expand Down