Permissions
Ask for operating system permissions during informed consent through a permission requester that your app provides.
A consent section can explain why the study needs an operating system (OS) permission, such as location or microphone access. With askPermission, the visual consent step asks for the permission while the participant reads that explanation.
Research Package decides when to ask, and your app decides how. The app provides a permission requester: a function that asks the OS and returns the outcome.
Research Package 4.0.0 does not talk to the OS itself, so apps that ask for no permissions link no permission plugins. The health plugin, for example, links Apple HealthKit, and App Store review rejects apps that link HealthKit without using it.
Version 3.2.0 in CAMS apps
CARP Mobile Sensing (CAMS) apps with carp_survey_package get research_package 3.2.0, which asks through permission_handler and health itself and takes no requester. In 3.2.0, healthDataTypes takes HealthDataType values. The sections, results and platform setup on this page apply to 3.2.0 too.
How it works
With askPermission: true, the visual consent step asks for the permissions of a section when the participant taps NEXT on it:
sequenceDiagram
participant V as Visual consent step
participant P as RPPermissions
participant R as Your requester
participant O as Operating system
Note over V: participant taps NEXT
loop each permission of the section
V->>P: request()
P->>R: requester()
R->>O: permission dialog
O-->>R: answer
R-->>P: status
P-->>V: status
end
Note over V: shows the next section
The sequence has these stages:
- The participant taps NEXT on a section that lists permissions.
- For each permission of the section, in list order, the step calls
RPPermissions.request, which calls your requester. A permission that the participant already granted in this step is not asked for again. - Your requester shows the OS dialog and returns an
RPPermissionStatus. - The step records the status and shows the next section, also after a denial.
- When the participant leaves the last section, the step adds an
RPPermissionResultwith every status to the task result.
BACK is hidden on a section until its permissions have been asked for, so the participant cannot leave the explanation without seeing the dialog. When the participant goes back and taps NEXT on the section again, the step asks again for every permission that is not granted.
Declare the permissions
List the permissions in permissions on the section that explains them, and set askPermission: true on the visual consent step:
The section type sets only the title and the illustration, so a section asks for nothing without permissions. A section may list several permissions, which the step asks for one at a time. RPPermissionType has these values:
| Permission type | Asks for | Section type |
|---|---|---|
location | Location while the app is in use | Location |
locationAlways | Location also while the app is in the background | Location |
microphone | The microphone | Microphone |
camera | The camera | Camera |
notification | Showing notifications | Notification |
activityRecognition | The physical activity of the participant, such as walking or running | ActivityRecognition |
sensors | Body sensors, such as heart rate, on Android, and motion data on iOS | — |
bluetooth | Bluetooth, for example to connect to a wearable | Bluetooth |
ignoreBatteryOptimizations | An exemption from battery optimization, so that sensing keeps running in the background (Android only) | BackgroundSensing |
health | Read access to the health data types in healthDataTypes | Health or HealthDataCollection |
For the section types, see Section types.
Write a requester
A requester has the type RPPermissionRequester: a function that takes an RPPermissionType and an optional named healthDataTypes list, and returns a Future<RPPermissionStatus>.
A requester built on the permission_handler plugin covers every permission type except health. Add version 12 of the plugin:
Version 13 of the plugin requires compileSdk 37 on Android, which is higher than the Flutter default.
Then add lib/permissions.dart, a requester based on the permissions.dart of the example app. It reads no health data, so it does not link HealthKit:
Most types map to one permission_handler permission. Four need more:
locationAlways: asks for foreground location first, because both platforms grant background location only after it.bluetooth: asks on Android forbluetoothScanandbluetoothConnectin one dialog, and reports the first status that is notgranted.activityRecognition: asks on iOS for the motion sensors, because iOS has no activity recognition permission.ignoreBatteryOptimizations: isunsupportedon iOS. On Android, the system asks the participant to exempt the app from battery optimization.
The limited and provisional statuses of permission_handler, such as provisional notifications on iOS, count as granted.
Register the requester
Pass the requester to ResearchPackage.ensureInitialized at the start of main():
RPPermissions.request() passes each request to the requester, and handles two failures:
- Without a requester, every permission is recorded as
unsupported, and no dialog appears. Only the console says why, with a message that ends incannot be requested, because no RPPermissions.requester has been set. - An exception in the requester is recorded as
unknown, and the consent flow continues.
A later call of ensureInitialized() without a requester keeps the one set earlier. You can also set RPPermissions.requester directly.
Declare healthDataTypes as an optional named parameter with a default, {List<String> healthDataTypes = const []}. A requester with a required parameter does not match RPPermissionRequester, and the app does not compile.
Ask for health data
Health data is not one permission: HealthKit and Google Health Connect grant access per data type. A section that lists the health permission names its data types in healthDataTypes, and the requester asks for them through the health plugin:
Add the health plugin
The plugin requires Android
minSdk26 or later and iOS 15.0 or later.Configure Android and iOS for health data
Android iOS Health Connect needs these changes:
minSdk26 or later inandroid/app/build.gradle.kts;- a
MainActivitythat extendsFlutterFragmentActivity; - one read permission per data type, such as
android.permission.health.READ_STEPS; - the Health Connect package and its rationale intent under
<queries>; - the rationale intent filter on
MainActivityand theViewPermissionUsageActivityactivity alias.
For the full setup, see Installation in the Health Sampling section.
Add the HealthKit capability to the
Runnertarget, and theNSHealthShareUsageDescriptionandNSHealthUpdateUsageDescriptionkeys toios/Runner/Info.plist. Without the capability, the HealthKit sheet does not appear. See iOS in the Health section.Ask for health data in the requester
Add this function to
lib/permissions.dart, and returnawait requestHealthData(healthDataTypes)in theRPPermissionType.healthcase:List the data types on a section
Each entry is the name of a
HealthDataType, such as'STEPS'. Ahealthpermission withouthealthDataTypesasks for nothing and is recorded asunsupported.
The requester asks for read access only. It records these outcomes:
- On Android, Health Connect reports the real outcome. When Health Connect is not available, the requester opens its page in the app store and records
unsupported. - On iOS,
grantedmeans only that the HealthKit sheet appeared. HealthKit does not tell an app whether the participant allowed read access, so check the data that arrives instead.
A misspelled data type name throws in the requester, and the permission is recorded as unknown.
Configure Android and iOS
Each permission needs an entry on each platform. The requester on this page needs these entries:
| Permission type | Android manifest | iOS Info.plist key | CocoaPods macro |
|---|---|---|---|
location | ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION | NSLocationWhenInUseUsageDescription | PERMISSION_LOCATION_WHENINUSE=1 |
locationAlways | ACCESS_FINE_LOCATION and, on Android 10 or later, ACCESS_BACKGROUND_LOCATION | NSLocationWhenInUseUsageDescription and NSLocationAlwaysAndWhenInUseUsageDescription | PERMISSION_LOCATION=1 |
microphone | RECORD_AUDIO | NSMicrophoneUsageDescription | PERMISSION_MICROPHONE=1 |
camera | CAMERA | NSCameraUsageDescription | PERMISSION_CAMERA=1 |
notification | POST_NOTIFICATIONS, on Android 13 or later | None | PERMISSION_NOTIFICATIONS=1 |
activityRecognition | ACTIVITY_RECOGNITION, on Android 10 or later | NSMotionUsageDescription | PERMISSION_SENSORS=1 |
sensors | BODY_SENSORS | NSMotionUsageDescription | PERMISSION_SENSORS=1 |
bluetooth | BLUETOOTH_SCAN and BLUETOOTH_CONNECT; BLUETOOTH on Android 11 or earlier | NSBluetoothAlwaysUsageDescription | PERMISSION_BLUETOOTH=1 |
ignoreBatteryOptimizations | REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | None | None |
health | See Ask for health data | NSHealthShareUsageDescription and NSHealthUpdateUsageDescription | None |
Android
Declare each permission in android/app/src/main/AndroidManifest.xml, above <application>. For the location section on this page:
A permission that the manifest does not declare is recorded as denied, and no dialog appears. Below Android 13, notification shows no dialog either: its status says whether notifications are turned on for the app.
iOS
Add a usage description for each permission to ios/Runner/Info.plist. iOS shows it in the permission dialog:
How permission_handler enables a permission depends on how the app links its plugins:
The plugin enables each permission that has its usage description in Info.plist, and notification by default. Build with flutter run or flutter build ios. A build started from Xcode cannot find Info.plist and leaves out every permission except notification, so their requests are recorded as permanentlyDenied.
Enable each permission with its macro in the post_install block of ios/Podfile. A permission without its macro is recorded as permanentlyDenied, and no dialog appears:
Read the permission statuses
The visual consent step records its RPPermissionResult under its own identifier. Its statuses map holds one RPPermissionStatus for each permission that the step asked for:
A denial never blocks the consent flow, so read the statuses to know what the app may collect:
| Status | Meaning |
|---|---|
granted | The participant granted the permission. For health on iOS, the HealthKit sheet appeared. |
denied | The participant denied the permission, and the app may ask again. |
permanentlyDenied | The OS shows no dialog for the permission any more. The participant must grant it in the system settings. |
restricted | The OS does not allow the permission, for example because of parental controls. |
unsupported | The permission does not exist on this platform, health has no data types, or no requester is set. |
unknown | No status was available, for example because the requester threw an exception. |
The step adds the result only when it asked for at least one permission. The result is also in the partial result that onCancel receives after DISAGREE, because the visual consent step comes before the review step. In JSON, statuses uses the names of the values:
startDate is when the visual consent step appeared, and endDate is when the participant left its last section.
Ask outside a consent flow
RPPermissions.request() also works outside a consent flow, through the same requester. For example, to ask for notifications when the participant turns on reminders:
For health, pass the data types as well: RPPermissions.request(RPPermissionType.health, healthDataTypes: ['STEPS']). The call never throws.
Troubleshooting
Pass the requester to ResearchPackage.ensureInitialized(): without a requester, every status is unsupported. Also check that the visual consent step has askPermission: true and that the section lists the permission in permissions. See Register the requester.
Declare the permission in android/app/src/main/AndroidManifest.xml. An undeclared permission is recorded as denied without a dialog.
With CocoaPods, enable the macro of the permission in ios/Podfile, as in the Podfile example. With Swift Package Manager, add the usage description to Info.plist, then delete ~/Library/Developer/Xcode/DerivedData, so that the plugin reads the file again.
Build with flutter run or flutter build ios. With Swift Package Manager, permission_handler finds Info.plist only in builds started from the project folder. To build from Xcode, run these commands in the project folder, then restart Xcode:
Add healthDataTypes to the section that lists health. On Android, also check that Health Connect is installed: when it is missing, the requester opens its store page and records unsupported. After installing it, the participant can go back to the section and tap NEXT to be asked again.