Conversation
Devices connected before anyone asked for permissions, so connect failed its permission check and the executor stayed PausedButShouldBeResumed until the app was restarted. Move the ask above the connect. Permissions are now declared as data (`DeviceManager.permissions`) and requested in one place by an injectable `PermissionRequester`, replacing 5 hand-rolled `onRequestPermissions()` implementations and 3 separate request mechanisms. Also fixes the "allow alarms & reminders" dialog on first launch (notifications auto-degrade to inexact scheduling) and Android's location ladder (locationWhenInUse is now always asked before locationAlways).
…oyment Three concurrent askers made permission dialogs fail silently: the deployment handler fires twice per launch, the location plugin requested background location natively, and probes initialized mid-ask. Android denies - without showing - any request made while a dialog is up. Now there is exactly one moment for each permission: - study-wide permissions (notification, measures) are asked when the deployment is configured, before probes initialize - device permissions are asked when the *user* connects the device, via requestPermissions() + connect() from the app UI connect() only checks - never asks - so the auto-connect paths (deployment, task start) can no longer pop dialogs unprompted. An ungranted device stays disconnected until the user connects it; the choice is persisted and later launches reconnect silently. Behaviour change: permission-guarded devices no longer sample until the user connects them once. Apps without a device-connect UI must add one. Also: LocationManager never initiates permission UI (background mode is enabled only once locationAlways is granted), the deployment handler is serialized against double-firing, and a failed permission request or deployment no longer poisons the queue behind it.
Permissions were declared on data types, which made the domain layer import permission_handler and grouped unrelated capabilities together: a study collecting step count also asked for SMS and call-log access, because they rode on the same phone device. Device managers now declare their own permissions, and the phone's capabilities are split into services a protocol declares individually - ActivityService, MicrophoneService, CameraService, PhoneLogService, TextMessageService, CalendarService, BluetoothScanService - each following the existing LocationService pattern. Data types that moved to a service of their own would leave probes with no device to sample through, so addMissingServiceDevices() adds the services a deployment's measures need but its protocol does not declare. Protocols from before API level 3.0 keep working unchanged. Notification permission moves to SmartPhoneClientManager.configure(): it is the app's own permission, not any device's, and Android 13+ shows nothing at all without it.
Zeroupper
marked this pull request as draft
September 4, 2026 09:59
Contributor
Author
|
Moving to draft. This design moves permissions onto device managers, which means the phone's capabilities become separate services ( |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #609.
The problem
Permission dialogs failed silently, and a study asked for more than it used.
Everyone asked for their own permissions on their own schedule — the study controller batch-asked at deployment, every probe asked again in
onResume(),LocationManagerasked through the location plugin, each device manager hand-rolled its own request. Android shows one dialog at a time and silently denies any request that arrives while another is up, so concurrent asks were dropped without a trace.Underneath that was an ownership problem. Permissions were declared on data types (
CamsDataTypeMetaData.permissions), which forced the domain layer to importpermission_handler, and grouped unrelated capabilities together: a study that wanted the phone's step count also asked for SMS and call-log access, because they all rode on the same phone device.What changed
Device managers declare permissions; data types no longer do.
DeviceManager.permissionsis the single declaration for OS permissions.CamsDataTypeMetaData.permissionsis deleted, and with it thepermission_handlerimport inlib/domain.dart— removing that dependency from the protocol model the Kotlin/TS backends share.The base
Probepermission API is gone.Probe.permissionsandhasRequiredPermissions()are deleted, along with their 7 call sites. A probe samples through its device manager, which is only connected once its permissions are granted — so if a probe is running, it is permissioned.Phone capabilities are split into services a protocol declares individually:
ActivityServiceactivityRecognitionMicrophoneServicemicrophoneCameraServicecameraPhoneLogServicephoneTextMessageServicesmsCalendarServicecalendarFullAccessBluetoothScanServicebluetoothScan+locationAlways; iOS:bluetoothEach follows the existing
LocationServicepattern — no new concepts.permission_handlerrequests are centralised and serialized.SmartPhoneClientManager.requestPermissions()is a queue: one dialog at a time,locationWhenInUseinserted beforelocationAlways, and a failed request can't block the next one.connect()only checks, never asks. Devices auto-connect on deployment and on task start; if those could ask, dialogs would appear unprompted. The app asks explicitly —requestPermissions(), thenconnect()— when the user chooses to connect a device.Old protocols keep working. Data types that moved to a service of their own would leave probes with no device to sample through, so
addMissingServiceDevices()adds the services a deployment's measures need but its protocol doesn't declare.Protocol API level → 3.0. From 3.0 a protocol is expected to declare the services it samples through; deployments that don't are repaired by the shim above.
Why this is better
A study asks for what its protocol declares. Permission scope used to be a side effect of which sampling package a data type happened to live in. Now it follows the services in the protocol: collecting step count no longer drags SMS and call-log permissions along with it — which is the difference between a study a participant accepts and one they abandon at the dialog.
The scope is readable before it runs. Because devices declare their own permissions and protocols declare their devices,
requiredPermissionslists what the deployment's connectable devices will ask for as soon as the deployment is received, before any system dialog appears.Dialogs stop being dropped. One serialized queue replaces the concurrent askers, so no
permission_handlerrequest is silently denied for arriving while another dialog is up. And a permission is asked for when the participant chooses to connect the device it belongs to, rather than in an unexplained burst at study start.One layering violation removed. Permission types are out of the domain model, so
lib/domain.dartno longer imports a platform permission plugin.Not covered by this
Two probes still request permissions on their own, through their own plugins rather than
permission_handler:CalendarProbe(viadevice_calendar) andHealthProbe(via Health Connect, which is also whyHealthServiceManageroverridesonRequestPermissions()). Those stay as they are here.Device permissions are asked when the user connects the device from the app UI, not during deployment. On fresh installs, services like location, weather and air quality stay disconnected until connected once; the grant persists, and later launches reconnect silently.
SCHEDULE_EXACT_ALARMis no longer requested — exact scheduling is used when the permission happens to be granted, otherwise notifications schedule inexactly (still delivered while idle, within minutes rather than to the second).Notification permission is still requested, but now from
SmartPhoneClientManager.configure()whenenableNotificationsis set, instead of from the notification manager — it is the app's own permission, not any device's.Breaking → majors (CAMS 3.0.0).