Skip to content

Ask device permissions when the user connects, not on deployment - #607

Draft
Zeroupper wants to merge 3 commits into
mainfrom
feat/permission-orchestration
Draft

Zeroupper wants to merge 3 commits into
mainfrom
feat/permission-orchestration

Conversation

@Zeroupper

@Zeroupper Zeroupper commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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(), LocationManager asked 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 import permission_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.permissions is the single declaration for OS permissions. CamsDataTypeMetaData.permissions is deleted, and with it the permission_handler import in lib/domain.dart — removing that dependency from the protocol model the Kotlin/TS backends share.

  • The base Probe permission API is gone. Probe.permissions and hasRequiredPermissions() 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:

    service permissions
    ActivityService activityRecognition
    MicrophoneService microphone
    CameraService camera
    PhoneLogService phone
    TextMessageService sms
    CalendarService calendarFullAccess
    BluetoothScanService Android: bluetoothScan + locationAlways; iOS: bluetooth

    Each follows the existing LocationService pattern — no new concepts.

  • permission_handler requests are centralised and serialized. SmartPhoneClientManager.requestPermissions() is a queue: one dialog at a time, locationWhenInUse inserted before locationAlways, 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(), then connect() — 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, requiredPermissions lists 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_handler request 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.dart no 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 (via device_calendar) and HealthProbe (via Health Connect, which is also why HealthServiceManager overrides onRequestPermissions()). Those stay as they are here.

⚠️ Behaviour change

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_ALARM is 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() when enableNotifications is set, instead of from the notification manager — it is the app's own permission, not any device's.

Breaking → majors (CAMS 3.0.0).

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.
@Zeroupper Zeroupper changed the title Ask for permissions once, before devices connect Ask device permissions when the user connects, not on deployment Aug 25, 2026
@Zeroupper Zeroupper self-assigned this Aug 25, 2026
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
Zeroupper marked this pull request as draft September 4, 2026 09:59
@Zeroupper

Copy link
Copy Markdown
Contributor Author

Moving to draft. This design moves permissions onto device managers, which means the phone's capabilities become separate services (MicrophoneService, CameraService, ActivityService, ...) that a protocol has to declare via addConnectedDevice(..., phone). That is an API change (protocol API level 3.0) and needs a decision on how we structure and expose these services before the package splits are finished. The alternative that keeps the current data-type model is #614 + #615.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent, uncoordinated permission requests cause silent denials on Android

1 participant