carp-dk / carp-dk/carp.sensing-flutter

Concurrent, uncoordinated permission requests cause silent denials on Android

Open
#609 0 comments 0 reactions 0 assignees View on GitHub
bug carp_mobile_sensing
Dominant language
Dart
Stars
84
Forks
31
Avg merge
1d 8h
Merged PRs (30d)
6

Description

## Symptom

On a fresh install on Android, permission dialogs during study deployment fail **silently** — permissions come back `denied` without any dialog being shown. After two such bounces Android reports `permanentlyDenied`, and the only way out is manually enabling the permission in the phone's Settings. Location is never successfully asked at all, so location-dependent sampling (location, weather, air quality) silently never starts.

## Root cause: 5+ independent permission askers, running concurrently

Android allows **one** permission request at a time (`Activity.mHasCurrentPermissionsRequest`). Any request that arrives while a dialog is already up is answered immediately with an empty result — which `permission_handler` reports as `denied`, with no UI shown.

CAMS currently has several places that ask, independently and without coordination:

1. **`SmartphoneStudyController.askForAllPermissions()`** — batch-requests all measure permissions at deployment. Also, `DeviceDeploymentReceived` fires **twice** on a normal launch, so two batch requests race each other.
2. **Every `Probe.onResume()`** — calls `requestPermissions()` again, per probe, when sampling starts. Many probes resume at once.
3. **`LocationManager` (carp_context_package)** — asks through the `location` plugin *natively*, outside `permission_handler`, both in `requestPermission()` and implicitly via `enableBackgroundMode()`. Shared singleton for location, weather and air-quality services, so three services can trigger it in parallel on `onConnect()`.
4. **Each `DeviceManager.onRequestPermissions()`** — hand-rolled imperative check/request code per device manager (BLE, context services, health, …), called from `connect()`, which CAMS invokes automatically on deployment and on task start.
5. **`FlutterLocalNotificationManager.configure()`** — requests `notification` + `scheduleExactAlarm` on its own during client configuration.

Any two of these overlapping is enough to produce a silent denial. On a real launch, several overlap.

## Secondary problems

- **Location ladder ignored**: `locationAlways` must be preceded by a granted `locationWhenInUse` (Android 10+). Batch requests don't insert the ladder step, so `locationAlways` is denied outright. `askForAllPermissions()` "solves" this by *removing all location permissions from the request* — i.e. they are never asked.
- **Unprompted dialogs**: because `connect()` asks, permission dialogs pop up during automatic connects (deployment, background task start) without any user action — against both Android and iOS UX guidelines ("ask in context").
- **`SCHEDULE_EXACT_ALARM` requested unconditionally**, although Google Play only allows it for alarm-clock-type apps and most host apps don't declare it — the request can never succeed and just adds to the collision traffic.
- **Duplicated imperative code**: every device manager re-implements `onHasPermissions()` / `onRequestPermissions()` per platform instead of declaring what it needs.

## Proposed fix

One rule: **permissions are declared as data; asking happens in one serialized place, at the moment the permission is about to be used.**

| permission | asked by | when |
|---|---|---|
| study-wide (notification, measures) | `SmartphoneStudyController` | at deployment configuration, before probes initialize |
| device-specific (e.g. `locationAlways`, BLE) | app UI via `DeviceManager.requestPermissions()` | when the **user** connects the device |
| health | `HealthServiceManager` (Health Connect, not the OS) | when the user connects the health service |

- `DeviceManager.permissions` / `Probe.permissions` become declarative lists; the base classes check and request from them.
- A single serialized request queue on `SmartPhoneClientManager`: one dialog at a time, `locationWhenInUse` auto-inserted before `locationAlways`, a failed request doesn't block the queue.
- `connect()` and probes only **check** — an ungranted device stays disconnected until the user connects it from the app.
- `LocationManager` never initiates permission UI; background mode is only enabled once `locationAlways` is already granted.
- Exact alarm scheduling is used when granted, otherwise notifications fall back to inexact scheduling.

Fixed by #607.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with SmartphoneStudyController.askForAllPermissions(), Probe.onResume(), LocationManager, DeviceManager.onRequestPermissions(), and FlutterLocalNotificationManager.configure(). Read the permission flow and compare the proposed serialized queue, declarative permission lists, and user-triggered device requests with the implementation in #607. Done means the overlapping silent denials and unprompted requests described here are addressed.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, flutter
Domain
mobile
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.