Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ apps/carp_mobile_sensing_app/test/credentials.dart
backends/carp_backend/test/credentials.dart
apps/carp_mobile_sensing_app/lib/config.dart
backends/carp_webservices/test/_credentials.dart

# Local path overrides for developing against a sibling worktree - never committed.
pubspec_overrides.yaml
5 changes: 1 addition & 4 deletions apps/carp_mobile_sensing_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,7 @@ class Sensing {
/// Initialize and set up sensing.
Future<void> initialize() async {
// Configure the client manager with the deployment service specified based on deployment mode
await client.configure(
deploymentService: deploymentService,
askForPermissions: true,
);
await client.configure(deploymentService: deploymentService);

// Listen on the measurements stream and count measurements
client.measurements.listen((measurement) => samplingSize++);
Expand Down
1 change: 0 additions & 1 deletion apps/carp_mobile_sensing_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';

import 'package:flutter/material.dart' hide TimeOfDay;
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart' as ble;

import 'package:carp_serializable/carp_serializable.dart';
Expand Down
6 changes: 0 additions & 6 deletions apps/carp_mobile_sensing_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ class LoadingPage extends StatelessWidget {
///
/// Returns true when successfully done.
Future<bool> init(BuildContext context) async {
// Request location "always" permissions upfront.
// Note that this is a two-step process on Android, where the user first has
// to grant "when in use" permissions, and then "always" permissions.
await Permission.locationWhenInUse.request();
await Permission.locationAlways.request();

// Initialize and use the CAWS backend if not in local deployment mode
if (bloc.deploymentMode != DeploymentMode.local) {
await CarpBackend().initialize();
Expand Down
13 changes: 2 additions & 11 deletions apps/carp_mobile_sensing_app/lib/src/services/sensing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ class Sensing {
info('Initializing $runtimeType - mode: ${bloc.deploymentMode}');

// Configure the client manager using the deployment service above (local or CAWS).
await client.configure(
deploymentService: deploymentService,
askForPermissions: true,
);
await client.configure(deploymentService: deploymentService);

// Listen on the measurements stream and count measurements and print them as they come in.
client.measurements.listen((measurement) {
Expand All @@ -107,13 +104,7 @@ class Sensing {
);

// Resume the current [study].
Future<void> resume() async {
// Need to ask for permissions before resuming, otherwise the app may crash
// when trying to start sampling without permissions.
controller?.askForAllPermissions().then((_) async {
controller?.resume();
});
}
Future<void> resume() async => controller?.resume();

// Pause the current [study].
Future<void> pause() async => client.pause();
Expand Down
1 change: 0 additions & 1 deletion apps/carp_mobile_sensing_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dependencies:
system_info2: ^4.0.0
path_provider: ^2.0.0
sqflite: ^2.2.8 # For local storage in SQLite DB
permission_handler: '>=11.0.0 <13.0.0' # For requesting permissions on Android and iOS
shared_preferences: ^2.2.0
package_info_plus: ^10.2.1
flutter_local_notifications: ^21.0.0 # For sending notification on AppTask
Expand Down
4 changes: 4 additions & 0 deletions backends/carp_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.0.0

* require `carp_mobile_sensing` ^3.0.0

## 2.1.1

- upgrade to `research_package` ^3.0.0
Expand Down
6 changes: 3 additions & 3 deletions backends/carp_backend/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: carp_backend
version: 2.1.1
version: 3.0.0
description: CARP data backend for CARP Mobile Sensing. Supports downloading study deployments and uploading data from/to a CARP Web Service (CAWS) server.
homepage: https://github.com/cph-cachet/carp.sensing-flutter
repository: https://github.com/carp-dk/carp.sensing-flutter/tree/main/backends/carp_backend
Expand Down Expand Up @@ -30,8 +30,8 @@ dependencies:

carp_serializable: ^3.0.0
carp_core: ^2.2.0
carp_mobile_sensing: ^2.3.0
carp_webservices: ^4.2.0
carp_mobile_sensing: ^3.0.0
carp_webservices: ^5.0.0

research_package: ^3.0.0

Expand Down
4 changes: 4 additions & 0 deletions backends/carp_webservices/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 5.0.0

* require `carp_mobile_sensing` ^3.0.0

## 4.2.0

* require `carp_serializable` ^3.0.0, which replaces the built-in `Uuid` with the [uuid](https://pub.dev/packages/uuid) package
Expand Down
2 changes: 1 addition & 1 deletion backends/carp_webservices/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies:
sdk: flutter

carp_core: ^2.1.2
carp_mobile_sensing: ^2.1.2
carp_mobile_sensing: ^3.0.0
qr_code_scanner_plus: ^2.0.12
oidc: ^0.14.0

Expand Down
4 changes: 2 additions & 2 deletions backends/carp_webservices/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: carp_webservices
version: 4.2.0
version: 5.0.0
description: Flutter API for accessing the CARP web services, including authentication, deployments, data, files, and collections of documents.
homepage: https://github.com/cph-cachet/carp.sensing-flutter
repository: https://github.com/carp-dk/carp.sensing-flutter/tree/main/backends/carp_webservices
Expand All @@ -26,7 +26,7 @@ dependencies:

carp_serializable: ^3.0.0
carp_core: ^2.2.0
carp_mobile_sensing: ^2.2.0
carp_mobile_sensing: ^3.0.0

http: ^1.6.0
json_annotation: ^4.12.0
Expand Down
73 changes: 73 additions & 0 deletions carp_mobile_sensing/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
## 3.0.0

Permissions are now declared as data, and requested at the moment they are about to
be used - study-wide permissions when the deployment is configured, device permissions
when the *user* connects the device.

**Who asks, when**

| Permission for | Asked when | Asked by |
|---|---|---|
| notifying app tasks (`notification`) | deployment configured | `SmartphoneStudyController` |
| measures on this phone (e.g. `activityRecognition`) | deployment configured | `SmartphoneStudyController` |
| a device/service (e.g. `locationAlways`) | the user connects it | the app UI, via `DeviceManager.requestPermissions()` then `connect()` |

CAMS auto-connects devices on deployment and on task start; those paths *check*
permissions and never ask, so no dialog can appear unprompted. All requests go through
one serialized queue - Android denies, without showing, any permission request that
arrives while another dialog is up, which used to make dialogs "fail silently".

**Behaviour change - devices no longer sample until the user connects them**

Previously a deployment tried to obtain all its permissions up front, and every
device with granted permissions started sampling automatically. Now a device whose
permissions have not been granted stays `disconnected` - silently, by design - until
the user connects it from the app (which asks first). **If your app has no UI for
connecting devices, location/weather/air quality and other permission-guarded devices
will never start.** Once connected, `isConnected` is persisted in the device
registration and later launches reconnect silently - the dialog is a one-time,
user-initiated event.

**Fixes**

* permission dialogs no longer fail silently. Three code paths asked concurrently
(the deployment handler fires twice per launch, the location plugin asked natively,
probes initialized mid-ask); Android bounces every request made while a dialog is
up, returning `denied` without showing anything
* no more "allow alarms & reminders" dialog on first launch - notifications are scheduled
exactly when `SCHEDULE_EXACT_ALARM` happens to be granted, and inexactly (still delivered
while the phone is idle, within minutes) when it is not. Drop `SCHEDULE_EXACT_ALARM` from
your manifest unless your study truly needs to-the-second reminders
* the Android location ladder works: `locationAlways` is asked only after `locationWhenInUse`,
in its own dialog, whatever order a study declares them in
* `Permission.notification` is asked for when a study with notifying app tasks starts,
instead of at `configure()` before any study exists
* a failed permission request or deployment configuration no longer blocks the
requests/configurations queued behind it

**Breaking**

* `DeviceManager.onRequestPermissions()` -> `List<Permission> get permissions`:

```dart
// before
Future<void> onRequestPermissions() async => await Permission.sensors.request();
// after
List<Permission> get permissions => [Permission.sensors];
```

`onHasPermissions()` now checks these by default - override it only to require a subset.
Devices not using `permission_handler` (e.g. Health Connect) can still override
`onRequestPermissions()`
* `SmartPhoneClientManager.configure(askForPermissions: bool)` ->
`configure(permissionRequester: PermissionRequester?)`. The default asks one dialog at a
time; pass your own to show a rationale first, or `null` to handle permissions in the app
* `SmartphoneStudyController.askForAllPermissions()` removed - study-wide permissions are
asked automatically at deployment; device permissions when the user connects the device.
The new `requiredPermissions` getter lists everything a deployment needs, so an app can
explain it up front
* `SmartphoneStudyController.permissions` removed - it cached a status the OS can revoke at
any time. Ask `permission_handler` instead
* `Probe.requestPermissions()` and `Probe.arePermissionsGranted()` removed - probes check
via `hasRequiredPermissions()` and never ask

## 2.3.1

* fix `duplicate column name: record_id` crash in the `record_id` SQLite migration (`SQLiteDataManager.onUpgrade`) by only adding the column/index when it isn't already there
Expand Down
6 changes: 3 additions & 3 deletions carp_mobile_sensing/example/lib/examples.dart
Original file line number Diff line number Diff line change
Expand Up @@ -452,12 +452,12 @@ void example_3() async {
// * [FlutterLocalNotificationController]
// * [SmartphoneDeploymentService]
// * [DeviceController]
// * asking for permissions
// * notifications enabled
// * asking for the permissions a study needs, one dialog at a time
await client.configure();

// disabling notifications and permissions handling
await client.configure(enableNotifications: false, askForPermissions: false);
// disabling notifications, and handling permissions in the app instead
await client.configure(enableNotifications: false, permissionRequester: null);

// add and deploy the protocol
final study = await client.addStudyFromProtocol(protocol);
Expand Down
11 changes: 0 additions & 11 deletions carp_mobile_sensing/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,6 @@ class StudyPageState extends State<StudyPage> {
// measures are available. This must happen before configuring the client.
SamplingPackageRegistry().register(ContextSamplingPackage());

// Request location permission BEFORE configuring the client. CAMS connects
// the LocationService device during deployment/startup, and that connect
// fails (and is never retried) if permission isn't already granted — which
// leaves the location task unable to resume.
await LocationManager().configure(locationService);
await LocationManager().requestPermission();

// Configure the client. Note that the client can take a series of configuration
// parameters, but here we're just using the default configurations.
await client.configure();
Expand Down Expand Up @@ -229,10 +222,6 @@ class StudyPageState extends State<StudyPage> {
if (study.isSampling) {
controller?.pause();
} else {
// CAMS does not request location permission itself, so ask for it here
// before sampling starts. This is what pops the OS location dialog.
await LocationManager().configure(locationService);
await LocationManager().requestPermission();
controller?.resume();
}
setState(() {});
Expand Down
3 changes: 2 additions & 1 deletion carp_mobile_sensing/lib/carp_mobile_sensing.json.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void _registerFromJsonFunctions() {
);
FromJsonFactory().register(
SmartphoneRegistration(),
type: '${DeviceConfiguration.DEVICE_NAMESPACE}.SmartphoneDeviceRegistration',
type:
'${DeviceConfiguration.DEVICE_NAMESPACE}.SmartphoneDeviceRegistration',
);

// Task classes
Expand Down
5 changes: 3 additions & 2 deletions carp_mobile_sensing/lib/domain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import 'package:carp_mobile_sensing/carp_mobile_sensing.dart';

import 'package:flutter/foundation.dart';
import 'package:flutter/cupertino.dart' show AppLifecycleState;
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:json_annotation/json_annotation.dart';
// Needed only by the 'infrastructure/services/device_info_service.dart' part
// below, which is an infrastructure file living in this library.
import 'package:device_info_plus/device_info_plus.dart';

part 'domain/core/smartphone_protocol.dart';
part 'domain/core/study_description.dart';
Expand Down
31 changes: 8 additions & 23 deletions carp_mobile_sensing/lib/domain/core/data_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,32 @@ enum DataEventType {
///
/// In addition to core [DataTypeMetaData], which stores the [type], [displayName],
/// and [timeType] of the data, this [CamsDataTypeMetaData] also stores
/// information on [dataEventType] and what [permissions] are needed on
/// runtime to collect this data type.
/// information on the [dataEventType].
///
/// Note that a data type does **not** declare permissions. Permissions belong
/// to the device that collects the data - see [DeviceManager.permissions] -
/// and are requested when that device is connected.
class CamsDataTypeMetaData extends DataTypeMetaData {
/// How a data type is collected (one-time or event-based).
DataEventType dataEventType;

/// The list of permissions that are required for this data type.
///
/// Note that this is the list of permissions needed for the probe collecting
/// this data type. It **should not** include permission to access a device
/// itself, such as Bluetooth permissions.
/// Such permissions should be handled on the app level.
///
/// See [PermissionGroup](https://pub.dev/documentation/permission_handler/latest/permission_handler/PermissionGroup-class.html)
/// for a list of possible permissions.
///
/// For Android permission in the Manifest.xml file,
/// see [Manifest.permission](https://developer.android.com/reference/android/Manifest.permission.html)
List<Permission> permissions;

/// Create a new description of a data [type] with some [displayName].
///
/// Default [timeType] is [DataTimeType.POINT],
/// default [dataEventType] is [DataEventType.EVENT], and
/// default [permissions] is empty (no permissions required).
/// Default [timeType] is [DataTimeType.POINT] and
/// default [dataEventType] is [DataEventType.EVENT].
CamsDataTypeMetaData({
required super.type,
super.displayName,
super.timeType,
this.dataEventType = DataEventType.EVENT,
this.permissions = const [],
});

/// Create a new description of a data type based on the [dataTypeMetaData].
///
/// Default [dataEventType] is [DataEventType.EVENT], and
/// default [permissions] is empty (no permissions required).
/// Default [dataEventType] is [DataEventType.EVENT].
CamsDataTypeMetaData.fromDataTypeMetaData({
required DataTypeMetaData dataTypeMetaData,
this.dataEventType = DataEventType.EVENT,
this.permissions = const [],
}) : super(
type: dataTypeMetaData.type,
displayName: dataTypeMetaData.displayName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ class SmartphoneDeployment extends PrimaryDeviceDeployment
return measures;
}

/// Does this deployment have a task that notifies the user?
/// Android 13+ needs permission for that.
bool get hasNotifyingTask =>
tasks.any((task) => task is AppTask && task.notification);

/// Get the [DeviceConfiguration] based on the [roleName].
/// This includes both the primary device and the connected devices.
/// Returns null if no device with [roleName] is found.
Expand Down
7 changes: 6 additions & 1 deletion carp_mobile_sensing/lib/domain/core/smartphone_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,12 @@ class SmartphoneStudyProtocol extends StudyProtocol
/// The API level used by study protocols.
/// This reflects the **major** version of the CARP Mobile Sensing framework
/// as set in the pubspec.yaml file.
static const String CAMS_PROTOCOL_API_LEVEL = '2.0';
///
/// From API level 3.0, a protocol declares the services its measures sample
/// through - `addConnectedDevice(ActivityService(), phone)` - which is what
/// makes the permissions a study needs visible in the protocol itself.
/// Protocols from earlier levels do not, and have them added on deployment.
static const String CAMS_PROTOCOL_API_LEVEL = '3.0';

// These static app names can be used as [applicationName] in the protocol.
// It is the name of the Flutter app as specified in the pubspec.yaml file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ abstract class NotificationManager {
'Notifications about scheduled tasks that the user has to do.';

/// Configure and set up the notification manager.
/// Also tries to get permissions to send notifications.
///
/// Does not ask for permission to notify - [SmartPhoneClientManager.configure]
/// does, before calling this.
Future<void> configure();

/// Create an immediate notification with [id], [title], and [body].
Expand Down
Loading