From 205d05791b8b85e6bf34eec905882fc4cf84e81d Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Mon, 15 Apr 2024 10:56:54 +0200 Subject: [PATCH 1/6] Request necessary permissions for `flutter_local_notifications` plugin --- carp_mobile_sensing/lib/runtime/client_manager.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/carp_mobile_sensing/lib/runtime/client_manager.dart b/carp_mobile_sensing/lib/runtime/client_manager.dart index 058ade95..bdbf5fb9 100644 --- a/carp_mobile_sensing/lib/runtime/client_manager.dart +++ b/carp_mobile_sensing/lib/runtime/client_manager.dart @@ -218,6 +218,10 @@ class SmartPhoneClientManager extends SmartphoneClient /// Should be called before sensing is started, if not already done as part of /// [configure]. Future askForAllPermissions() async { + // needed for local notifications + SamplingPackageRegistry().permissions.add(Permission.notification); + SamplingPackageRegistry().permissions.add(Permission.scheduleExactAlarm); + if (SamplingPackageRegistry().permissions.isNotEmpty) { info('Asking for permission for all measure types.'); permissions = await SamplingPackageRegistry().permissions.request(); From e3236afcb6e0485189d2d0437fea364878a0bad0 Mon Sep 17 00:00:00 2001 From: bardram Date: Mon, 15 Apr 2024 20:01:27 +0200 Subject: [PATCH 2/6] fix of #376 --- carp_mobile_sensing/example/lib/example.dart | 3 ++ .../lib/runtime/app_task_controller.dart | 38 +++++++++++++++---- .../runtime/executors/executor_factory.dart | 19 ++-------- .../lib/runtime/executors/executors.dart | 5 --- .../executors/task_control_executors.dart | 13 ++----- .../lib/runtime/executors/task_executors.dart | 12 +++++- .../local_notification_controller.dart | 12 +----- 7 files changed, 52 insertions(+), 50 deletions(-) diff --git a/carp_mobile_sensing/example/lib/example.dart b/carp_mobile_sensing/example/lib/example.dart index dc25e557..08dc66d4 100644 --- a/carp_mobile_sensing/example/lib/example.dart +++ b/carp_mobile_sensing/example/lib/example.dart @@ -840,6 +840,9 @@ void appTaskControllerExample() async { case UserTaskState.done: userTask.executor.stop(); break; + case UserTaskState.notified: + print('Task id: ${userTask.id} was clicked in the OS.'); + break; default: // break; diff --git a/carp_mobile_sensing/lib/runtime/app_task_controller.dart b/carp_mobile_sensing/lib/runtime/app_task_controller.dart index 266d73ae..b8677c46 100644 --- a/carp_mobile_sensing/lib/runtime/app_task_controller.dart +++ b/carp_mobile_sensing/lib/runtime/app_task_controller.dart @@ -122,7 +122,7 @@ class AppTaskController { }) async { if (_userTaskFactories[executor.task.type] == null) { warning( - 'Could not enqueue AppTask. Could not find a factory for creating ' + '$runtimeType - Could not enqueue AppTask. Could not find a factory for creating ' "a UserTask for type '${executor.task.type}'"); return null; } else { @@ -155,12 +155,13 @@ class AppTaskController { void dequeue(String id) { UserTask? userTask = _userTaskMap[id]; if (userTask == null) { - warning("Could not dequeue AppTask - id is not valid: '$id'"); + warning( + "$runtimeType - Could not dequeue AppTask - id is not valid: '$id'"); } else { userTask.state = UserTaskState.dequeued; _userTaskMap.remove(id); _controller.sink.add(userTask); - info('Dequeued $userTask'); + info('$runtimeType - Dequeued $userTask'); if (notificationsEnabled) { SmartPhoneClientManager() @@ -170,6 +171,25 @@ class AppTaskController { } } + /// Callback when a notification in the OS is clicked. + void onNotification(String id) { + UserTask? userTask = getUserTask(id); + if (userTask != null) { + info('$runtimeType - User Task notification clicked - $userTask'); + + // only notify if this task is still active + if (userTask.state == UserTaskState.enqueued || + userTask.state == UserTaskState.canceled) { + userTask.state = UserTaskState.notified; + _controller.sink.add(userTask); + userTask.onNotification(); + } + } else { + warning( + "$runtimeType - Error in callback from notification - no task with id '$id' found."); + } + } + /// Mark the [UserTask] with [id] as done. /// [result] may contain the result obtained from the task. /// Note that a done task remains on the queue. If you want to remove a @@ -177,12 +197,13 @@ class AppTaskController { void done(String id, [Data? result]) { UserTask? userTask = _userTaskMap[id]; if (userTask == null) { - warning("Could not find AppTask - id is not valid: '$id'"); + warning( + "$runtimeType - Could not find User Task - id is not valid: '$id'"); } else { userTask.state = UserTaskState.done; userTask.result = result; _controller.sink.add(userTask); - info('Marked $userTask as done'); + info('$runtimeType - Marked $userTask as done'); SmartPhoneClientManager() .notificationController @@ -196,13 +217,14 @@ class AppTaskController { void expire(String id) { UserTask? userTask = _userTaskMap[id]; if (userTask == null) { - warning("Could not expire AppTask - id is not valid: '$id'"); + warning( + "$runtimeType - Could not expire AppTask - id is not valid: '$id'"); } else { // only expire tasks which are not already done or expired if (userTask.state != UserTaskState.done) { userTask.state = UserTaskState.expired; _controller.sink.add(userTask); - info('Expired $userTask'); + info('$runtimeType - Expired $userTask'); } SmartPhoneClientManager() .notificationController @@ -255,7 +277,7 @@ class AppTaskController { // now put the restored task back on the queue if (_userTaskFactories[executor.task.type] == null) { warning( - 'Could not enqueue AppTask. Could not find a factory for creating ' + '$runtimeType - Could not enqueue AppTask. Could not find a factory for creating ' "a UserTask for type '${executor.task.type}'"); } else { UserTask userTask = diff --git a/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart b/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart index ba3d084c..8828fcae 100644 --- a/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart +++ b/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart @@ -13,7 +13,6 @@ class ExecutorFactory { factory ExecutorFactory() => _instance; final Map _triggerExecutors = {}; - final Map _taskExecutors = {}; /// Returns the relevant [TaskControlExecutor] based on the type of [trigger] /// and [task]. @@ -107,22 +106,10 @@ class ExecutorFactory { return _triggerExecutors[triggerId]!; } - /// Get the [TaskExecutor] for a [task]. - TaskExecutor? getTaskExecutor(TaskConfiguration task) => - _taskExecutors[task.name]; - /// Create a [TaskExecutor] for a [task] based on the task type. - /// If already created, returns this. TaskExecutor createTaskExecutor(TaskConfiguration task) { - if (_taskExecutors[task.name] == null) { - TaskExecutor executor = BackgroundTaskExecutor(); - if (task is AppTask) { - executor = AppTaskExecutor(); - } else if (task is FunctionTask) { - executor = FunctionTaskExecutor(); - } - _taskExecutors[task.name] = executor; - } - return _taskExecutors[task.name]!; + if (task is AppTask) return AppTaskExecutor(); + if (task is FunctionTask) return FunctionTaskExecutor(); + return BackgroundTaskExecutor(); } } diff --git a/carp_mobile_sensing/lib/runtime/executors/executors.dart b/carp_mobile_sensing/lib/runtime/executors/executors.dart index a4c6bae6..88595f54 100644 --- a/carp_mobile_sensing/lib/runtime/executors/executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/executors.dart @@ -343,11 +343,6 @@ class _StartedState extends _InitializedState { @override ExecutorState get state => ExecutorState.started; - - @override - void start() => warning( - 'Trying to start a ${executor.runtimeType} but it is already started. ' - 'Ignoring this.'); } class _StoppedState extends _InitializedState { diff --git a/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart b/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart index ba6770ed..c501ca5b 100644 --- a/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart @@ -51,15 +51,10 @@ class TaskControlExecutor extends AbstractExecutor { ExecutorFactory().getTriggerExecutor(taskControl.triggerId); triggerExecutor?.triggerEvents.listen((_) => onTrigger()); - // if not already created, get the task executor and add the - // measurements it collects to the stream group - if (ExecutorFactory().getTaskExecutor(task) == null) { - taskExecutor = ExecutorFactory().createTaskExecutor(task); - taskExecutor?.initialize(task, deployment); - _group.add(taskExecutor!.measurements); - } else { - taskExecutor = ExecutorFactory().getTaskExecutor(task); - } + // get the task executor and add the measurements it collects to the stream group + taskExecutor = ExecutorFactory().createTaskExecutor(task); + taskExecutor?.initialize(task, deployment); + _group.add(taskExecutor!.measurements); return true; } diff --git a/carp_mobile_sensing/lib/runtime/executors/task_executors.dart b/carp_mobile_sensing/lib/runtime/executors/task_executors.dart index f0cd464a..78192cae 100644 --- a/carp_mobile_sensing/lib/runtime/executors/task_executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/task_executors.dart @@ -54,6 +54,13 @@ class BackgroundTaskExecutor extends TaskExecutor { // Early out if no probes. if (probes.isEmpty) return true; + // Early out if already running (this is a background task) + if (state == ExecutorState.started) { + warning( + 'Trying to start a $runtimeType but it is already started. Ignoring this.'); + return false; + } + // Check if the device for this task is connected. if (probes.first.deviceManager.isConnected) { if (configuration?.duration != null) { @@ -119,15 +126,16 @@ class AppTaskExecutor extends TaskExecutor { @override Future onStart() async { - // when an app task is started simply put it on the queue + // when an app task is started, create a UserTask and put it on the queue userTask = await AppTaskController().enqueue(this); + state; // = ExecutorState.stopped; return true; } @override Future onStop() async { backgroundTaskExecutor.stop(); - // if an app task is stopped, removed it from the queue again + // if an app task is stopped, remove the user task from the queue again if (userTask != null) AppTaskController().dequeue(userTask!.id); userTask = null; return true; diff --git a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart index 6af87313..34454210 100644 --- a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart @@ -168,22 +168,14 @@ class FlutterLocalNotificationController implements NotificationController { } } +/// Callback method called when a notification is clicked in the operating system. @pragma('vm:entry-point') void onDidReceiveNotificationResponse(NotificationResponse response) { String? payload = response.payload; - debug('NotificationController - callback on notification, payload: $payload'); if (payload != null) { - UserTask? task = AppTaskController().getUserTask(payload); - info('NotificationController - User Task notification selected - $task'); - if (task != null) { - task.state = UserTaskState.notified; - task.onNotification(); - } else { - warning( - 'NotificationController - Error in callback from notification - no task found.'); - } + AppTaskController().onNotification(payload); } else { warning( "NotificationController - Error in callback from notification - payload is '$payload'"); From 145812be92850040987ee95235436a4a551a65fa Mon Sep 17 00:00:00 2001 From: bardram Date: Tue, 16 Apr 2024 22:00:32 +0200 Subject: [PATCH 3/6] fix of #378 --- .../notification/local_notification_controller.dart | 7 ------- .../lib/runtime/notification/notification_controller.dart | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart index 34454210..61804987 100644 --- a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart @@ -105,7 +105,6 @@ class FlutterLocalNotificationController implements NotificationController { Future cancelNotification(int id) async => await FlutterLocalNotificationsPlugin().cancel(id); - /// Send an immediate notification for a [task]. @override Future createTaskNotification(UserTask task) async { if (task.notification) { @@ -120,7 +119,6 @@ class FlutterLocalNotificationController implements NotificationController { } } - /// Schedule a notification for a [task] at the [UserTask.triggerTime]. @override Future scheduleTaskNotification(UserTask task) async { // early out if not to be scheduled @@ -149,16 +147,11 @@ class FlutterLocalNotificationController implements NotificationController { } } - /// The number of pending notifications. - /// - /// Note that on iOS there is a limit of 64 pending notifications. - /// See https://pub.dev/packages/flutter_local_notifications#ios-pending-notifications-limit @override Future get pendingNotificationRequestsCount async => (await FlutterLocalNotificationsPlugin().pendingNotificationRequests()) .length; - /// Cancel (i.e., remove) the notification for the [task]. @override Future cancelTaskNotification(UserTask task) async { if (task.notification) { diff --git a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart index 21f6f9ba..d34a4557 100644 --- a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart @@ -75,6 +75,9 @@ abstract class NotificationController { Future cancelTaskNotification(UserTask task); /// The number of pending notifications. + /// + /// Note that on iOS there is a limit of 64 pending notifications. + /// See https://pub.dev/packages/flutter_local_notifications#ios-pending-notifications-limit Future get pendingNotificationRequestsCount; } From 1d3d51f580ca26185624a5706bab601d04407378 Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 18 Apr 2024 09:05:42 +0200 Subject: [PATCH 4/6] fix of notification controller --- carp_mobile_sensing/CHANGELOG.md | 7 ++ carp_mobile_sensing/README.md | 45 ++++++++++--- carp_mobile_sensing/analysis_options.yaml | 2 +- .../example/android/app/build.gradle | 1 - .../android/app/src/main/AndroidManifest.xml | 28 +++++++- carp_mobile_sensing/example/lib/example.dart | 10 +-- carp_mobile_sensing/example/lib/main.dart | 25 +++---- .../lib/carp_mobile_sensing.json.dart | 2 +- carp_mobile_sensing/lib/domain/triggers.dart | 12 ++-- .../lib/runtime/app_task_controller.dart | 2 +- .../lib/runtime/client_manager.dart | 19 ++---- .../lib/runtime/data_manager.dart | 3 +- .../lib/runtime/deployment_controller.dart | 3 +- .../lib/runtime/deployment_service.dart | 2 +- .../lib/runtime/device_controller.dart | 3 +- .../lib/runtime/device_manager.dart | 3 +- .../local_notification_controller.dart | 36 ++++++++++ .../notification/notification_controller.dart | 65 ++++++++++++++++--- .../lib/runtime/persistence.dart | 2 +- .../lib/runtime/sampling_package.dart | 7 +- carp_mobile_sensing/lib/runtime/settings.dart | 2 +- .../lib/runtime/study_manager.dart | 3 +- .../lib/runtime/user_tasks.dart | 2 +- .../lib/sampling_packages/device/device.dart | 8 +++ .../sampling_packages/device/device_data.dart | 2 +- .../device/device_package.dart | 9 ++- .../device/device_probes.dart | 2 +- .../sensors/light_probe.dart | 2 +- .../sensors/pedometer_probe.dart | 14 +++- .../sensors/sensor_data.dart | 5 +- .../sensors/sensor_package.dart | 14 +++- .../sensors/sensor_probes.dart | 4 +- .../sampling_packages/sensors/sensors.dart | 9 +++ carp_mobile_sensing/pubspec.yaml | 4 +- .../lib/carp_movesense_package.dart | 7 +- 35 files changed, 277 insertions(+), 87 deletions(-) diff --git a/carp_mobile_sensing/CHANGELOG.md b/carp_mobile_sensing/CHANGELOG.md index 19d9dde6..c45fa2d6 100644 --- a/carp_mobile_sensing/CHANGELOG.md +++ b/carp_mobile_sensing/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.5.0 + +* Improvements to the Notification Controller + * support app-specific methods to create, schedule, and cancel app-specific notifications + * asks for permission on Android (Issue [#375](https://github.com/cph-cachet/carp.sensing-flutter/issues/375)) + * tapping a notification takes the user to the right task in the app (Issue [#376](https://github.com/cph-cachet/carp.sensing-flutter/issues/376)) + ## 1.4.7 * improvements to handling connected BLE devices diff --git a/carp_mobile_sensing/README.md b/carp_mobile_sensing/README.md index 4f6cb414..7e8e39ec 100644 --- a/carp_mobile_sensing/README.md +++ b/carp_mobile_sensing/README.md @@ -28,19 +28,25 @@ dependencies: When you want to add CAMS to you app, there are a few things to do in terms of configuring your app. -First, since CAMS rely on the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) plugin, you should configure your app to the [platforms it supports](https://pub.dev/packages/flutter_local_notifications#-supported-platforms) and configure your app for both [Android](https://pub.dev/packages/flutter_local_notifications#-android-setup) and [iOS](https://pub.dev/packages/flutter_local_notifications#-ios-setup). +First, CAMS rely on the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) plugin. So **if you want to use App Tasks and notifications** you should configure your app to the [platforms it supports](https://pub.dev/packages/flutter_local_notifications#-supported-platforms) and configure your app for both [Android](https://pub.dev/packages/flutter_local_notifications#-android-setup) and [iOS](https://pub.dev/packages/flutter_local_notifications#-ios-setup). There is a lot of details in configuring for notifications - especially for Android - so read this carefully. ### Android Integration -Set the minimum android SDK to 23 and Java SDK Version to 33 by setting the `minSdkVersion`, the `compileSdkVersion`, and `targetSdkVersion` in the `build.gradle` file, located in the `android/app/` folder: +Set the minimum android SDK to 26 and Java SDK Version to 34 by setting the `minSdkVersion`, the `compileSdkVersion`, and `targetSdkVersion` in the `build.gradle` file, located in the `android/app/` folder: ```gradle android { - compileSdkVersion 33 + compileSdkVersion 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { - minSdkVersion 23 - targetSdkVersion 33 + ... + minSdkVersion 26 + targetSdkVersion flutter.targetSdkVersion ... } ... @@ -48,15 +54,38 @@ android { ``` The pedometer (step count) probe needs permission to `ACTIVITY_RECOGNITION`. -Schedule notifications (if using `AppTask`) needs permissions to `SCHEDULE_EXACT_ALARM` and `USE_EXACT_ALARM`. Add the following to your app's `manifest.xml` file located in `android/app/src/main`: +Scheduled notifications (if using `AppTask`) needs a set of permissions, such as `USE_EXACT_ALARM` and `VIBRATE`. +If collecting step counts or using notifications in your app, add the following to your app's `manifest.xml` file located in `android/app/src/main`: ````xml + - + + + + + + + ```` +Also specify the following between the `` tags so that the plugin can show the scheduled notifications: + +```xml + + + + + + + + + + +``` + ### iOS Integration The pedometer (step count) probe uses `NSMotion` on iOS and the `NSMotionUsageDescription` needs to be specified in the app's `Info.plist` file located in `ios/Runner`: diff --git a/carp_mobile_sensing/analysis_options.yaml b/carp_mobile_sensing/analysis_options.yaml index 1fbe7b3d..07e99cb8 100644 --- a/carp_mobile_sensing/analysis_options.yaml +++ b/carp_mobile_sensing/analysis_options.yaml @@ -16,4 +16,4 @@ linter: constant_identifier_names: false depend_on_referenced_packages: true avoid_print: false - use_string_in_part_of_directives: false + use_string_in_part_of_directives: true diff --git a/carp_mobile_sensing/example/android/app/build.gradle b/carp_mobile_sensing/example/android/app/build.gradle index ade0c044..3c079822 100644 --- a/carp_mobile_sensing/example/android/app/build.gradle +++ b/carp_mobile_sensing/example/android/app/build.gradle @@ -26,7 +26,6 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - // compileSdkVersion flutter.compileSdkVersion compileSdkVersion 34 compileOptions { diff --git a/carp_mobile_sensing/example/android/app/src/main/AndroidManifest.xml b/carp_mobile_sensing/example/android/app/src/main/AndroidManifest.xml index b9916abf..0bffe5ba 100644 --- a/carp_mobile_sensing/example/android/app/src/main/AndroidManifest.xml +++ b/carp_mobile_sensing/example/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,17 @@ - + - + + + + + + + + + + + + + + + + + + + + + diff --git a/carp_mobile_sensing/example/lib/example.dart b/carp_mobile_sensing/example/lib/example.dart index 08dc66d4..33984565 100644 --- a/carp_mobile_sensing/example/lib/example.dart +++ b/carp_mobile_sensing/example/lib/example.dart @@ -358,16 +358,18 @@ void example_3() async { final client = SmartPhoneClientManager(); // default configuration using: - // * [AwesomeNotificationController] + // * [FlutterLocalNotificationController] // * [SmartphoneDeploymentService] // * [DeviceController] + // * asking for permissions + // * notifications enabled await client.configure(); - // use flutter_local_notification for notifications + // disabling notifications, device heartbeat, and permissions handling await client.configure( - notificationController: FlutterLocalNotificationController(), - askForPermissions: false, enableNotifications: false, + heartbeat: false, + askForPermissions: false, ); // add and deploy the protocol diff --git a/carp_mobile_sensing/example/lib/main.dart b/carp_mobile_sensing/example/lib/main.dart index f4073fdd..8dbec3d0 100644 --- a/carp_mobile_sensing/example/lib/main.dart +++ b/carp_mobile_sensing/example/lib/main.dart @@ -297,24 +297,25 @@ class LocalStudyProtocolManager implements StudyProtocolManager { // phone, // ); - // Add two app tasks with notifications. + // Add app tasks with notifications. // // These App Tasks are added for demo purpose and you should see notifications // on the phone. However, nothing will happen when you click on it. // See the PulmonaryMonitor demo app for a full-scale example of how to use // the App Task model. - // Add a task 1 minute after deployment and make a notification. - // protocol.addTaskControl( - // ElapsedTimeTrigger(elapsedTime: const IsoDuration(seconds: 30)), - // AppTask( - // type: BackgroundSensingUserTask.ONE_TIME_SENSING_TYPE, - // title: "Elapsed Time - App Task", - // measures: [Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)], - // notification: true, - // ), - // phone, - // ); + // Add a task after deployment and make a notification. + protocol.addTaskControl( + ElapsedTimeTrigger(elapsedTime: const IsoDuration(seconds: 30)), + AppTask( + type: BackgroundSensingUserTask.ONE_TIME_SENSING_TYPE, + title: "Elapsed Time Trigger - App Task", + description: 'Collection of Device Information.', + measures: [Measure(type: DeviceSamplingPackage.DEVICE_INFORMATION)], + notification: true, + ), + phone, + ); // // Add a cron job every day at 11:45 // protocol.addTaskControl( diff --git a/carp_mobile_sensing/lib/carp_mobile_sensing.json.dart b/carp_mobile_sensing/lib/carp_mobile_sensing.json.dart index 3baed63d..67877c26 100644 --- a/carp_mobile_sensing/lib/carp_mobile_sensing.json.dart +++ b/carp_mobile_sensing/lib/carp_mobile_sensing.json.dart @@ -1,4 +1,4 @@ -part of carp_mobile_sensing; +part of 'carp_mobile_sensing.dart'; bool _fromJsonFunctionsRegistered = false; diff --git a/carp_mobile_sensing/lib/domain/triggers.dart b/carp_mobile_sensing/lib/domain/triggers.dart index 997334e3..032cd6cd 100644 --- a/carp_mobile_sensing/lib/domain/triggers.dart +++ b/carp_mobile_sensing/lib/domain/triggers.dart @@ -144,22 +144,22 @@ class DateTimeTrigger extends TriggerConfiguration implements Schedulable { /// Here are a couple of examples: /// /// ``` -/// // collect every day at 13:30 +/// // trigger every day at 13:30 /// RecurrentScheduledTrigger(type: RecurrentType.daily, time: TimeOfDay(hour: 13, minute: 30)); /// -/// // collect every other day at 13:30 +/// // trigger every other day at 13:30 /// RecurrentScheduledTrigger(type: RecurrentType.daily, separationCount: 1, time: TimeOfDay(hour: 13, minute: 30)); /// -/// // collect every wednesday at 12:23 +/// // trigger every wednesday at 12:23 /// RecurrentScheduledTrigger(type: RecurrentType.weekly, dayOfWeek: DateTime.wednesday, time: TimeOfDay(hour: 12, minute: 23)); /// -/// // collect every 2nd monday at 12:23 +/// // trigger every 2nd monday at 12:23 /// RecurrentScheduledTrigger(type: RecurrentType.weekly, dayOfWeek: DateTime.monday, separationCount: 1, time: TimeOfDay(hour: 12, minute: 23)); /// -/// // collect monthly in the second week on a monday at 14:30 +/// // trigger monthly in the second week on a monday at 14:30 /// RecurrentScheduledTrigger(type: RecurrentType.monthly, weekOfMonth: 2, dayOfWeek: DateTime.monday, time: TimeOfDay(hour: 14, minute: 30)); /// -/// // collect quarterly on the 11th day of the first month in each quarter at 21:30 +/// // trigger quarterly on the 11th day of the first month in each quarter at 21:30 /// RecurrentScheduledTrigger(type: RecurrentType.monthly, dayOfMonth: 11, separationCount: 2, time: TimeOfDay(hour: 21, minute: 30)); /// ``` /// diff --git a/carp_mobile_sensing/lib/runtime/app_task_controller.dart b/carp_mobile_sensing/lib/runtime/app_task_controller.dart index b8677c46..d43c734d 100644 --- a/carp_mobile_sensing/lib/runtime/app_task_controller.dart +++ b/carp_mobile_sensing/lib/runtime/app_task_controller.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of 'runtime.dart'; /// A controller of [UserTask]s which is accessible in the [userTaskQueue]. class AppTaskController { diff --git a/carp_mobile_sensing/lib/runtime/client_manager.dart b/carp_mobile_sensing/lib/runtime/client_manager.dart index bdbf5fb9..4c62e4bc 100644 --- a/carp_mobile_sensing/lib/runtime/client_manager.dart +++ b/carp_mobile_sensing/lib/runtime/client_manager.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of 'runtime.dart'; /// The possible states of the [SmartPhoneClientManager]. enum ClientManagerState { @@ -23,6 +23,7 @@ class SmartPhoneClientManager extends SmartphoneClient bool _heartbeat = true; final StreamGroup _group = StreamGroup.broadcast(); ClientManagerState _state = ClientManagerState.created; + Map? _permissions; /// The runtime state of this client manager. ClientManagerState get state => _state; @@ -33,7 +34,7 @@ class SmartPhoneClientManager extends SmartphoneClient Stream get measurements => _group.stream; /// The permissions granted to this client from the OS. - Map? permissions; + Map get permissions => _permissions ?? {}; SmartPhoneClientManager._() { WidgetsFlutterBinding.ensureInitialized(); @@ -89,6 +90,7 @@ class SmartPhoneClientManager extends SmartphoneClient /// If [askForPermissions] is true (default), this client manager will /// automatically ask for permissions for all sampling packages at once. /// If you want the app to handle permissions itself, set this to false. + /// You can later use the [askForAllPermissions] to ask for all permissions. /// /// If [heartbeat] is true, a [Heartbeat] data point will be uploaded for all /// devices (including the phone) in all studies running on this client @@ -218,18 +220,11 @@ class SmartPhoneClientManager extends SmartphoneClient /// Should be called before sensing is started, if not already done as part of /// [configure]. Future askForAllPermissions() async { - // needed for local notifications - SamplingPackageRegistry().permissions.add(Permission.notification); - SamplingPackageRegistry().permissions.add(Permission.scheduleExactAlarm); - if (SamplingPackageRegistry().permissions.isNotEmpty) { info('Asking for permission for all measure types.'); - permissions = await SamplingPackageRegistry().permissions.request(); - - for (var permission in SamplingPackageRegistry().permissions) { - PermissionStatus status = await permission.status; - info('Permissions for $permission : $status'); - } + _permissions = await SamplingPackageRegistry().permissions.request(); + permissions.forEach((permission, status) => + info('Permissions for $permission : $status')); } } diff --git a/carp_mobile_sensing/lib/runtime/data_manager.dart b/carp_mobile_sensing/lib/runtime/data_manager.dart index b2997af1..7bcae763 100644 --- a/carp_mobile_sensing/lib/runtime/data_manager.dart +++ b/carp_mobile_sensing/lib/runtime/data_manager.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of 'runtime.dart'; /// The [DataManager] interface is used to upload [Measurement] objects to any /// data manager that implements this interface. diff --git a/carp_mobile_sensing/lib/runtime/deployment_controller.dart b/carp_mobile_sensing/lib/runtime/deployment_controller.dart index dc002537..aaa31858 100644 --- a/carp_mobile_sensing/lib/runtime/deployment_controller.dart +++ b/carp_mobile_sensing/lib/runtime/deployment_controller.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of 'runtime.dart'; /// A [SmartphoneDeploymentController] controls the execution of a [SmartphoneDeployment]. class SmartphoneDeploymentController extends StudyRuntime { diff --git a/carp_mobile_sensing/lib/runtime/deployment_service.dart b/carp_mobile_sensing/lib/runtime/deployment_service.dart index a9c835b0..bb23c0c4 100644 --- a/carp_mobile_sensing/lib/runtime/deployment_service.dart +++ b/carp_mobile_sensing/lib/runtime/deployment_service.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of 'runtime.dart'; /// A local (in-memory) implementation of a [DeploymentService] useful in /// CAMS studies to be deployed locally on this phone. diff --git a/carp_mobile_sensing/lib/runtime/device_controller.dart b/carp_mobile_sensing/lib/runtime/device_controller.dart index 70588f2a..ebd07236 100644 --- a/carp_mobile_sensing/lib/runtime/device_controller.dart +++ b/carp_mobile_sensing/lib/runtime/device_controller.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of 'runtime.dart'; /// A [DeviceController] handles runtime management of all devices and services /// connected to this phone, including the phone itself. diff --git a/carp_mobile_sensing/lib/runtime/device_manager.dart b/carp_mobile_sensing/lib/runtime/device_manager.dart index a7e9a793..d6508afe 100644 --- a/carp_mobile_sensing/lib/runtime/device_manager.dart +++ b/carp_mobile_sensing/lib/runtime/device_manager.dart @@ -6,7 +6,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of 'runtime.dart'; /// A [DeviceManager] handles a hardware device or online service on runtime. abstract class DeviceManager diff --git a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart index 61804987..bffc83e7 100644 --- a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart @@ -29,6 +29,11 @@ class FlutterLocalNotificationController implements NotificationController { Future initialize() async { tz.initializeTimeZones(); + List permissions = + List.from([Permission.notification, Permission.scheduleExactAlarm]); + var status = await permissions.request(); + debug('$runtimeType - permissions: $status'); + await FlutterLocalNotificationsPlugin().initialize( const InitializationSettings( android: AndroidInitializationSettings('app_icon'), @@ -101,6 +106,37 @@ class FlutterLocalNotificationController implements NotificationController { return id; } + @override + Future scheduleRecurrentNotifications( + {int? id, + required String title, + String? body, + required RecurrentScheduledTrigger schedule}) async { + id ??= _random.nextInt(1000); + final time = tz.TZDateTime.from( + schedule.firstOccurrence, tz.getLocation(Settings().timezone)); + + DateTimeComponents recurrence = switch (schedule.type) { + RecurrentType.daily => DateTimeComponents.time, + RecurrentType.weekly => DateTimeComponents.dayOfWeekAndTime, + RecurrentType.monthly => DateTimeComponents.dayOfMonthAndTime, + }; + + await FlutterLocalNotificationsPlugin().zonedSchedule( + id, + title, + body, + time, + _platformChannelSpecifics, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: recurrence, + ); + + return id; + } + @override Future cancelNotification(int id) async => await FlutterLocalNotificationsPlugin().cancel(id); diff --git a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart index d34a4557..b5469fa5 100644 --- a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart @@ -6,8 +6,20 @@ */ part of runtime; -/// A controller of user notifications based on [UserTask]s. -/// Works closely with the [AppTaskController]. +/// A controller of user notifications allow for creating, scheduling, and +/// canceling user notifications. +/// +/// This controller serves two purposes: +/// +/// 1. It is used by the [AppTaskController] to send notification about +/// [UserTask]s (which are created based on [AppTask] in the [StudyProtocol]). +/// This happens automatically, if the app task is configured to +/// send a notification. +/// +/// 2. It can be used by the app to create, schedule, and cancel app-specific +/// notifications. This is done using the [createNotification], [scheduleNotification], +/// and [scheduleRecurrentNotifications] methods, which +/// creates an immediate, scheduled, or recurrent notification, respectively. abstract class NotificationController { /// The upper limit of scheduled notification on iOS. static const PENDING_NOTIFICATION_LIMIT = 64; @@ -44,7 +56,7 @@ abstract class NotificationController { /// Create an immediate notification with [id], [title], and [body]. /// If the [id] is not specified, a random id will be generated. /// - /// Returns the id of the notification. + /// Returns the id of the notification created. Future createNotification({ int? id, required String title, @@ -54,7 +66,7 @@ abstract class NotificationController { /// Schedule a notification with [id], [title], and [body] at the [schedule] time. /// If the [id] is not specified, a random id will be generated. /// - /// Returns the id of the notification. + /// Returns the id of the notification created. Future scheduleNotification({ int? id, required String title, @@ -62,6 +74,27 @@ abstract class NotificationController { required DateTime schedule, }); + /// Schedule recurrent notifications with [id], [title], and [body] at the + /// [schedule] time. + /// + /// Allows for daily, weekly, and monthly recurrence according to the [schedule]. + /// + /// Note that [RecurrentScheduledTrigger.separationCount] and + /// [RecurrentScheduledTrigger.end] are **not used**, i.e. days / + /// weeks / months cannot be skipped in the scheduled and the notifications + /// keeps recurring indefinitely. If you want to stop a recurrent notification + /// schedule, use the [cancelNotification] method. + /// + /// If the [id] is not specified, a random id will be generated. + /// + /// Returns the id of the notification created. + Future scheduleRecurrentNotifications({ + int? id, + required String title, + String? body, + required RecurrentScheduledTrigger schedule, + }); + /// Cancel (i.e., remove) the notification with [id]. Future cancelNotification(int id); @@ -84,8 +117,14 @@ abstract class NotificationController { /// A no-operation notification controller that does nothing. class NoOpNotificationController implements NotificationController { @override - Future createNotification( - {int? id, required String title, String? body}) async => + Future initialize() async {} + + @override + Future createNotification({ + int? id, + required String title, + String? body, + }) async => 0; @override @@ -97,12 +136,15 @@ class NoOpNotificationController implements NotificationController { 0; @override - Future cancelNotification(int id) async {} - @override - Future cancelTaskNotification(UserTask task) async {} + Future scheduleRecurrentNotifications( + {int? id, + required String title, + String? body, + required RecurrentScheduledTrigger schedule}) async => + 0; @override - Future initialize() async {} + Future cancelNotification(int id) async {} @override Future scheduleTaskNotification(UserTask task) async {} @@ -110,6 +152,9 @@ class NoOpNotificationController implements NotificationController { @override Future createTaskNotification(UserTask task) async {} + @override + Future cancelTaskNotification(UserTask task) async {} + @override Future get pendingNotificationRequestsCount async => 0; } diff --git a/carp_mobile_sensing/lib/runtime/persistence.dart b/carp_mobile_sensing/lib/runtime/persistence.dart index 246a903d..b66be321 100644 --- a/carp_mobile_sensing/lib/runtime/persistence.dart +++ b/carp_mobile_sensing/lib/runtime/persistence.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of 'runtime.dart'; /// A persistence layer that knows how to persistently store deployment and /// app task information across app restart. diff --git a/carp_mobile_sensing/lib/runtime/sampling_package.dart b/carp_mobile_sensing/lib/runtime/sampling_package.dart index 659d19e9..e4ea6dc7 100644 --- a/carp_mobile_sensing/lib/runtime/sampling_package.dart +++ b/carp_mobile_sensing/lib/runtime/sampling_package.dart @@ -1,4 +1,4 @@ -part of runtime; +part of 'runtime.dart'; /// A registry of [SamplingPackage] packages. /// @@ -152,6 +152,11 @@ abstract class SamplingPackage { /// The list of permissions that this package need in order to run. /// + /// Note that this is the list of permissions used for the probes in this + /// sampling package. It **should not** include permission to access the 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. /// diff --git a/carp_mobile_sensing/lib/runtime/settings.dart b/carp_mobile_sensing/lib/runtime/settings.dart index c6f4a244..e8ef64fb 100644 --- a/carp_mobile_sensing/lib/runtime/settings.dart +++ b/carp_mobile_sensing/lib/runtime/settings.dart @@ -1,4 +1,4 @@ -part of runtime; +part of 'runtime.dart'; /// Misc. settings for CAMS. /// diff --git a/carp_mobile_sensing/lib/runtime/study_manager.dart b/carp_mobile_sensing/lib/runtime/study_manager.dart index 62948bcd..36e014f1 100644 --- a/carp_mobile_sensing/lib/runtime/study_manager.dart +++ b/carp_mobile_sensing/lib/runtime/study_manager.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of 'runtime.dart'; /// An interface defining a manger of [SmartphoneStudyProtocol]s. /// diff --git a/carp_mobile_sensing/lib/runtime/user_tasks.dart b/carp_mobile_sensing/lib/runtime/user_tasks.dart index 7236c035..9cdd8f7c 100644 --- a/carp_mobile_sensing/lib/runtime/user_tasks.dart +++ b/carp_mobile_sensing/lib/runtime/user_tasks.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of 'runtime.dart'; /// A factory which can create a [UserTask] based on the `type` of an /// [AppTask]. diff --git a/carp_mobile_sensing/lib/sampling_packages/device/device.dart b/carp_mobile_sensing/lib/sampling_packages/device/device.dart index e9db3e89..d93f7eed 100644 --- a/carp_mobile_sensing/lib/sampling_packages/device/device.dart +++ b/carp_mobile_sensing/lib/sampling_packages/device/device.dart @@ -1,5 +1,13 @@ +/* + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the + * Technical University of Denmark (DTU). + * Use of this source code is governed by a MIT-style license that can be + * found in the LICENSE file. + */ + /// A library containing a sampling package for collecting information from the /// device hardware: +/// /// - device info /// - battery status /// - screen events diff --git a/carp_mobile_sensing/lib/sampling_packages/device/device_data.dart b/carp_mobile_sensing/lib/sampling_packages/device/device_data.dart index d5acb566..cb9d55bb 100644 --- a/carp_mobile_sensing/lib/sampling_packages/device/device_data.dart +++ b/carp_mobile_sensing/lib/sampling_packages/device/device_data.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of device; +part of 'device.dart'; /// Holds basic information about the mobile device from where the data is collected. /// diff --git a/carp_mobile_sensing/lib/sampling_packages/device/device_package.dart b/carp_mobile_sensing/lib/sampling_packages/device/device_package.dart index 26f04a12..6a186273 100644 --- a/carp_mobile_sensing/lib/sampling_packages/device/device_package.dart +++ b/carp_mobile_sensing/lib/sampling_packages/device/device_package.dart @@ -1,4 +1,11 @@ -part of device; +/* + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the + * Technical University of Denmark (DTU). + * Use of this source code is governed by a MIT-style license that can be + * found in the LICENSE file. + */ + +part of 'device.dart'; class DeviceSamplingPackage extends SmartphoneSamplingPackage { /// Measure type for collection of basic device information like device name, diff --git a/carp_mobile_sensing/lib/sampling_packages/device/device_probes.dart b/carp_mobile_sensing/lib/sampling_packages/device/device_probes.dart index ece35d7f..ca8cfb1d 100644 --- a/carp_mobile_sensing/lib/sampling_packages/device/device_probes.dart +++ b/carp_mobile_sensing/lib/sampling_packages/device/device_probes.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of device; +part of 'device.dart'; /// The [BatteryProbe] listens to the hardware battery and collect a [BatteryState] /// every time the battery state changes. For example, battery level or charging mode. diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/light_probe.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/light_probe.dart index d5672c5f..8931c75a 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/light_probe.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/light_probe.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of sensors; +part of 'sensors.dart'; /// The [LightProbe] listens to the phone's light sensor typically located /// near the front camera. diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/pedometer_probe.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/pedometer_probe.dart index ff1834a6..9f9395a0 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/pedometer_probe.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/pedometer_probe.dart @@ -1,11 +1,11 @@ /* - * Copyright 2018-2022 Copenhagen Center for Health Technology (CACHET) at the + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the * Technical University of Denmark (DTU). * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of sensors; +part of 'sensors.dart'; /// The pedometer probe listens to the hardware step counter sensor. /// @@ -15,6 +15,16 @@ part of sensors; /// Note that the [Pedometer] plugin returns the total steps taken since last /// system boot. class PedometerProbe extends StreamProbe { + @override + Future onStart() async { + // Ask for permission before starting probe. + var status = await Permission.activityRecognition.request(); + + return (status == PermissionStatus.granted) + ? super.onStart() + : Future.value(false); + } + @override Stream get stream => pedometer.Pedometer.stepCountStream.map( (pedometer.StepCount count) => Measurement.fromData( diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_data.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_data.dart index bbd18361..191c0c56 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_data.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_data.dart @@ -1,10 +1,11 @@ /* - * Copyright 2024 Copenhagen Center for Health Technology (CACHET) at the + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the * Technical University of Denmark (DTU). * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of sensors; + +part of 'sensors.dart'; /// Ambient light intensity in Lux. /// Typically collected from the light sensor on the front of the phone. diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_package.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_package.dart index 21bd935d..dd82902e 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_package.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_package.dart @@ -1,4 +1,11 @@ -part of sensors; +/* + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the + * Technical University of Denmark (DTU). + * Use of this source code is governed by a MIT-style license that can be + * found in the LICENSE file. + */ + +part of 'sensors.dart'; class SensorSamplingPackage extends SmartphoneSamplingPackage { /// Rate of change in velocity, including gravity, along perpendicular x, y, @@ -48,6 +55,11 @@ class SensorSamplingPackage extends SmartphoneSamplingPackage { /// * No sampling configuration needed. static const String STEP_COUNT = CarpDataTypes.STEP_COUNT_TYPE_NAME; + @override + List get permissions => [ + Permission.activityRecognition, + ]; + @override DataTypeSamplingSchemeMap get samplingSchemes => DataTypeSamplingSchemeMap.from([ diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_probes.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_probes.dart index b37df2f1..fdf835a4 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_probes.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/sensor_probes.dart @@ -1,11 +1,11 @@ /* - * Copyright 2018-2024 Copenhagen Center for Health Technology (CACHET) at the + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the * Technical University of Denmark (DTU). * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of sensors; +part of 'sensors.dart'; /// An abstract sensor probe used by all sensor probes to get the [samplingPeriod]. /// diff --git a/carp_mobile_sensing/lib/sampling_packages/sensors/sensors.dart b/carp_mobile_sensing/lib/sampling_packages/sensors/sensors.dart index e788dd6f..7699a69f 100644 --- a/carp_mobile_sensing/lib/sampling_packages/sensors/sensors.dart +++ b/carp_mobile_sensing/lib/sampling_packages/sensors/sensors.dart @@ -1,5 +1,13 @@ +/* + * Copyright 2018 Copenhagen Center for Health Technology (CACHET) at the + * Technical University of Denmark (DTU). + * Use of this source code is governed by a MIT-style license that can be + * found in the LICENSE file. + */ + /// A library containing a sampling package for collecting data from the basic /// device sensors: +/// /// - accelerometer /// - gyroscope /// - magnetometer @@ -18,6 +26,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pedometer/pedometer.dart' as pedometer; import 'package:statistics/statistics.dart'; import 'package:sample_statistics/sample_statistics.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; import 'package:carp_core/carp_core.dart'; diff --git a/carp_mobile_sensing/pubspec.yaml b/carp_mobile_sensing/pubspec.yaml index 627cd4a2..a8ea6f6a 100644 --- a/carp_mobile_sensing/pubspec.yaml +++ b/carp_mobile_sensing/pubspec.yaml @@ -1,6 +1,6 @@ name: carp_mobile_sensing description: Mobile Sensing Framework for Flutter. A software framework for collecting sensor data from the phone and attached wearable devices via probes. Can be extended. -version: 1.4.7 +version: 1.5.0 homepage: https://github.com/cph-cachet/carp.sensing-flutter environment: @@ -25,7 +25,7 @@ dependencies: archive: ^3.3.0 permission_handler: ^11.0.0 shared_preferences: ^2.2.0 - package_info_plus: ^6.0.0 + package_info_plus: '>=6.0.0 <8.0.0' flutter_local_notifications: ^17.0.0 # For sending notification on AppTask cron: ^0.6.0 # For scheduling / triggering cron jobs timezone: ^0.9.0 # For collecting time zone information diff --git a/packages/carp_movesense_package/lib/carp_movesense_package.dart b/packages/carp_movesense_package/lib/carp_movesense_package.dart index 556510ce..86021a53 100644 --- a/packages/carp_movesense_package/lib/carp_movesense_package.dart +++ b/packages/carp_movesense_package/lib/carp_movesense_package.dart @@ -129,12 +129,7 @@ class MovesenseSamplingPackage implements SamplingPackage { } @override - List get permissions => [ - Permission.location, - Permission.bluetooth, - Permission.bluetoothConnect, - Permission.bluetoothScan, - ]; + List get permissions => []; @override DataTypeSamplingSchemeMap get samplingSchemes => From 5fb15dc9592037ea40a9a3cf379fde739dac0828 Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 18 Apr 2024 12:10:36 +0200 Subject: [PATCH 5/6] linter + fix of #379 --- .../local/console_data_manager.dart | 2 +- .../local/file_data_manager.dart | 2 +- .../local/file_study_manager.dart | 2 +- .../local/sqlite_data_manager.dart | 2 +- carp_mobile_sensing/lib/domain/app_task.dart | 2 +- carp_mobile_sensing/lib/domain/data.dart | 3 ++- .../lib/domain/data_endpoint.dart | 2 +- .../lib/domain/data_types.dart | 3 ++- .../lib/domain/device_configurations.dart | 2 +- .../lib/domain/device_info.dart | 2 +- carp_mobile_sensing/lib/domain/domain.dart | 11 ++++++--- .../lib/domain/sampling_configurations.dart | 3 ++- .../lib/domain/smartphone_deployment.dart | 2 +- .../lib/domain/study_description.dart | 2 +- .../lib/domain/study_protocol.dart | 2 +- carp_mobile_sensing/lib/domain/tasks.dart | 2 +- .../lib/domain/transformers.dart | 3 ++- carp_mobile_sensing/lib/domain/triggers.dart | 3 ++- .../lib/runtime/deployment_controller.dart | 5 ++-- .../executors/deployment_executor.dart | 2 +- .../runtime/executors/executor_factory.dart | 2 +- .../lib/runtime/executors/executors.dart | 2 +- .../lib/runtime/executors/probes.dart | 2 +- .../executors/task_control_executors.dart | 2 +- .../lib/runtime/executors/task_executors.dart | 2 +- .../runtime/executors/trigger_executors.dart | 23 +++++++++++-------- .../local_notification_controller.dart | 3 ++- .../notification/notification_controller.dart | 3 ++- .../lib/runtime/util/cron_parser.dart | 2 +- 29 files changed, 58 insertions(+), 40 deletions(-) diff --git a/carp_mobile_sensing/lib/data_managers/local/console_data_manager.dart b/carp_mobile_sensing/lib/data_managers/local/console_data_manager.dart index 52b8a15c..d1fb273b 100644 --- a/carp_mobile_sensing/lib/data_managers/local/console_data_manager.dart +++ b/carp_mobile_sensing/lib/data_managers/local/console_data_manager.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of data_managers; +part of '../data_managers.dart'; /// A very simple data manager that just "uploads" the data to the /// console (i.e., prints it). Used mainly for testing and debugging purposes. diff --git a/carp_mobile_sensing/lib/data_managers/local/file_data_manager.dart b/carp_mobile_sensing/lib/data_managers/local/file_data_manager.dart index 521005bf..79a788c6 100644 --- a/carp_mobile_sensing/lib/data_managers/local/file_data_manager.dart +++ b/carp_mobile_sensing/lib/data_managers/local/file_data_manager.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of data_managers; +part of '../data_managers.dart'; class FileDataManagerFactory implements DataManagerFactory { @override diff --git a/carp_mobile_sensing/lib/data_managers/local/file_study_manager.dart b/carp_mobile_sensing/lib/data_managers/local/file_study_manager.dart index db3f066c..ec325503 100644 --- a/carp_mobile_sensing/lib/data_managers/local/file_study_manager.dart +++ b/carp_mobile_sensing/lib/data_managers/local/file_study_manager.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of data_managers; +part of '../data_managers.dart'; /// Retrieve and store [StudyProtocol] json definitions on the device's local /// file system. diff --git a/carp_mobile_sensing/lib/data_managers/local/sqlite_data_manager.dart b/carp_mobile_sensing/lib/data_managers/local/sqlite_data_manager.dart index f22dfce0..69f0578a 100644 --- a/carp_mobile_sensing/lib/data_managers/local/sqlite_data_manager.dart +++ b/carp_mobile_sensing/lib/data_managers/local/sqlite_data_manager.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of data_managers; +part of '../data_managers.dart'; class SQLiteDataManagerFactory implements DataManagerFactory { @override diff --git a/carp_mobile_sensing/lib/domain/app_task.dart b/carp_mobile_sensing/lib/domain/app_task.dart index 0268dae2..b73798c6 100644 --- a/carp_mobile_sensing/lib/domain/app_task.dart +++ b/carp_mobile_sensing/lib/domain/app_task.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// A task that notifies the app when it is triggered. /// diff --git a/carp_mobile_sensing/lib/domain/data.dart b/carp_mobile_sensing/lib/domain/data.dart index ba54036e..57f31feb 100644 --- a/carp_mobile_sensing/lib/domain/data.dart +++ b/carp_mobile_sensing/lib/domain/data.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of domain; + +part of 'domain.dart'; /// A [Data] object holding a link to a file. @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) diff --git a/carp_mobile_sensing/lib/domain/data_endpoint.dart b/carp_mobile_sensing/lib/domain/data_endpoint.dart index 5c09c701..12e5fbe2 100644 --- a/carp_mobile_sensing/lib/domain/data_endpoint.dart +++ b/carp_mobile_sensing/lib/domain/data_endpoint.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// Specify an endpoint where a [DataManager] can upload data. @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) diff --git a/carp_mobile_sensing/lib/domain/data_types.dart b/carp_mobile_sensing/lib/domain/data_types.dart index 191b1b00..0e463f57 100644 --- a/carp_mobile_sensing/lib/domain/data_types.dart +++ b/carp_mobile_sensing/lib/domain/data_types.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of domain; + +part of 'domain.dart'; /// Contains CAMS data type definitions similar to CARP Core [CarpDataTypes]. class CAMSDataType { diff --git a/carp_mobile_sensing/lib/domain/device_configurations.dart b/carp_mobile_sensing/lib/domain/device_configurations.dart index c84c34ab..260fc3ab 100644 --- a/carp_mobile_sensing/lib/domain/device_configurations.dart +++ b/carp_mobile_sensing/lib/domain/device_configurations.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// An online service which works as a "software device" in a protocol. @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) diff --git a/carp_mobile_sensing/lib/domain/device_info.dart b/carp_mobile_sensing/lib/domain/device_info.dart index aca8a8ae..c9606052 100644 --- a/carp_mobile_sensing/lib/domain/device_info.dart +++ b/carp_mobile_sensing/lib/domain/device_info.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// Provides (static) information about the local device. /// diff --git a/carp_mobile_sensing/lib/domain/domain.dart b/carp_mobile_sensing/lib/domain/domain.dart index e1918f7b..3d26bd2d 100644 --- a/carp_mobile_sensing/lib/domain/domain.dart +++ b/carp_mobile_sensing/lib/domain/domain.dart @@ -1,7 +1,13 @@ +/* + * Copyright 2018-2022 Copenhagen Center for Health Technology (CACHET) at the + * Technical University of Denmark (DTU). + * Use of this source code is governed by a MIT-style license that can be + * found in the LICENSE file. + */ + /// The CAMS implementation of the core CARP domain classes like /// [StudyProtocol], [TaskConfiguration], and [Measure]. -/// Also hold JSON serialization and deseralization logic to handle seraialization -/// of the domain objects. +/// Also hold JSON logic to handle de/serialization of the domain objects. library domain; import 'dart:io'; @@ -22,7 +28,6 @@ part 'smartphone_deployment.dart'; part 'app_task.dart'; part 'tasks.dart'; part 'triggers.dart'; -// part 'datum.dart'; part 'data.dart'; part 'data_types.dart'; part 'device_info.dart'; diff --git a/carp_mobile_sensing/lib/domain/sampling_configurations.dart b/carp_mobile_sensing/lib/domain/sampling_configurations.dart index e1d930d1..eb36ea81 100644 --- a/carp_mobile_sensing/lib/domain/sampling_configurations.dart +++ b/carp_mobile_sensing/lib/domain/sampling_configurations.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of domain; + +part of 'domain.dart'; /// A sampling configuration that saves the last time it was sampled. @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) diff --git a/carp_mobile_sensing/lib/domain/smartphone_deployment.dart b/carp_mobile_sensing/lib/domain/smartphone_deployment.dart index e4417302..11ac8976 100644 --- a/carp_mobile_sensing/lib/domain/smartphone_deployment.dart +++ b/carp_mobile_sensing/lib/domain/smartphone_deployment.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// Contains the entire description and configuration for how a smartphone /// device participates in the deployment of a study on a smartphone. diff --git a/carp_mobile_sensing/lib/domain/study_description.dart b/carp_mobile_sensing/lib/domain/study_description.dart index 08d108b7..1d42994c 100644 --- a/carp_mobile_sensing/lib/domain/study_description.dart +++ b/carp_mobile_sensing/lib/domain/study_description.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) class StudyDescription extends Serializable { diff --git a/carp_mobile_sensing/lib/domain/study_protocol.dart b/carp_mobile_sensing/lib/domain/study_protocol.dart index 1a8c421e..3dcd1540 100644 --- a/carp_mobile_sensing/lib/domain/study_protocol.dart +++ b/carp_mobile_sensing/lib/domain/study_protocol.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// A mixin holding smartphone-specific data for a [SmartphoneStudyProtocol] and /// [SmartphoneDeployment]. diff --git a/carp_mobile_sensing/lib/domain/tasks.dart b/carp_mobile_sensing/lib/domain/tasks.dart index db20279b..a67d1e9f 100644 --- a/carp_mobile_sensing/lib/domain/tasks.dart +++ b/carp_mobile_sensing/lib/domain/tasks.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of domain; +part of 'domain.dart'; /// Signature of Dart function that have no arguments and returns no data. typedef VoidFunction = void Function(); diff --git a/carp_mobile_sensing/lib/domain/transformers.dart b/carp_mobile_sensing/lib/domain/transformers.dart index 3c12a92d..c924ddd2 100644 --- a/carp_mobile_sensing/lib/domain/transformers.dart +++ b/carp_mobile_sensing/lib/domain/transformers.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of domain; + +part of 'domain.dart'; /// Signature of a data transformer. typedef DataTransformer = Data Function(Data); diff --git a/carp_mobile_sensing/lib/domain/triggers.dart b/carp_mobile_sensing/lib/domain/triggers.dart index 032cd6cd..0ff7df47 100644 --- a/carp_mobile_sensing/lib/domain/triggers.dart +++ b/carp_mobile_sensing/lib/domain/triggers.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of domain; + +part of 'domain.dart'; /// A trigger that does nothing. @JsonSerializable(fieldRename: FieldRename.none, includeIfNull: false) diff --git a/carp_mobile_sensing/lib/runtime/deployment_controller.dart b/carp_mobile_sensing/lib/runtime/deployment_controller.dart index aaa31858..0e602c5f 100644 --- a/carp_mobile_sensing/lib/runtime/deployment_controller.dart +++ b/carp_mobile_sensing/lib/runtime/deployment_controller.dart @@ -275,6 +275,9 @@ class SmartphoneDeploymentController extends StudyRuntime { /// [configure] must be called before starting sampling. @override void start([bool start = true]) { + info( + '$runtimeType - Starting data sampling for study deployment: ${deployment?.studyDeploymentId}'); + // if this study has not yet been deployed, do this first. if (status.index < StudyStatus.Deployed.index) { tryDeployment().then((value) { @@ -287,8 +290,6 @@ class SmartphoneDeploymentController extends StudyRuntime { super.start(); if (start) _executor.start(); } - info( - '$runtimeType - Starting data sampling for study deployment: ${deployment?.studyDeploymentId}'); } /// Stop this controller and data sampling. diff --git a/carp_mobile_sensing/lib/runtime/executors/deployment_executor.dart b/carp_mobile_sensing/lib/runtime/executors/deployment_executor.dart index eb560c16..2e46da22 100644 --- a/carp_mobile_sensing/lib/runtime/executors/deployment_executor.dart +++ b/carp_mobile_sensing/lib/runtime/executors/deployment_executor.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; /// The [SmartphoneDeploymentExecutor] is responsible for executing a [SmartphoneDeployment]. /// For each task control in this deployment, it starts a [TaskControlExecutor]. diff --git a/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart b/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart index 8828fcae..cad18740 100644 --- a/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart +++ b/carp_mobile_sensing/lib/runtime/executors/executor_factory.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; class ExecutorFactory { static final ExecutorFactory _instance = ExecutorFactory._(); diff --git a/carp_mobile_sensing/lib/runtime/executors/executors.dart b/carp_mobile_sensing/lib/runtime/executors/executors.dart index 88595f54..3b39c4aa 100644 --- a/carp_mobile_sensing/lib/runtime/executors/executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/executors.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; //--------------------------------------------------------------------------------------- // EXECUTORS diff --git a/carp_mobile_sensing/lib/runtime/executors/probes.dart b/carp_mobile_sensing/lib/runtime/executors/probes.dart index fc3debda..0b353aed 100644 --- a/carp_mobile_sensing/lib/runtime/executors/probes.dart +++ b/carp_mobile_sensing/lib/runtime/executors/probes.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; /// A [Probe] is a specialized [Executor] responsible for collecting data from /// the device sensors as configured in a [Measure]. diff --git a/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart b/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart index c501ca5b..ec40354a 100644 --- a/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/task_control_executors.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; /// Responsible for handling the execution of a [TaskControl]. /// diff --git a/carp_mobile_sensing/lib/runtime/executors/task_executors.dart b/carp_mobile_sensing/lib/runtime/executors/task_executors.dart index 78192cae..92995bef 100644 --- a/carp_mobile_sensing/lib/runtime/executors/task_executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/task_executors.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; /// The [TaskExecutor] is responsible for executing a [TaskConfiguration]. /// For each measure in the task, it looks up an appropriate [Probe] to diff --git a/carp_mobile_sensing/lib/runtime/executors/trigger_executors.dart b/carp_mobile_sensing/lib/runtime/executors/trigger_executors.dart index a9da44c9..16b56232 100644 --- a/carp_mobile_sensing/lib/runtime/executors/trigger_executors.dart +++ b/carp_mobile_sensing/lib/runtime/executors/trigger_executors.dart @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -part of runtime; +part of '../runtime.dart'; class TriggerEvent { // TriggerConfiguration? trigger; @@ -43,9 +43,14 @@ abstract class TriggerExecutor Future onStart() async => true; @override - Future onRestart() async => true; + @mustCallSuper + Future onRestart() async { + _timer?.cancel(); + return true; + } @override + @mustCallSuper Future onStop() async { _timer?.cancel(); return true; @@ -176,9 +181,9 @@ class PeriodicTriggerExecutor @override Future onStart() async { - _timer = Timer.periodic(configuration!.period, (t) { - onTrigger(); - }); + _timer = Timer.periodic(configuration!.period, (_) => onTrigger()); + debug('$this - starting timer: ${_timer.hashCode}'); + return true; } } @@ -275,7 +280,7 @@ class CronScheduledTriggerExecutor @override Future onStop() async { _task?.cancel(); - return true; + return super.onStop(); } } @@ -310,7 +315,7 @@ class SamplingEventTriggerExecutor @override Future onStop() async { _subscription?.cancel(); - return true; + return super.onStop(); } } @@ -335,7 +340,7 @@ class ConditionalSamplingEventTriggerExecutor @override Future onStop() async { _subscription?.cancel(); - return true; + return super.onStop(); } } @@ -498,6 +503,6 @@ class UserTaskTriggerExecutor extends TriggerExecutor { @override Future onStop() async { _subscription?.cancel(); - return true; + return super.onStop(); } } diff --git a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart index bffc83e7..2d05af3b 100644 --- a/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/local_notification_controller.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of '../runtime.dart'; /// A [NotificationController] based on the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) /// Flutter plugin. diff --git a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart index b5469fa5..3de07920 100644 --- a/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart +++ b/carp_mobile_sensing/lib/runtime/notification/notification_controller.dart @@ -4,7 +4,8 @@ * Use of this source code is governed by a MIT-style license that can be * found in the LICENSE file. */ -part of runtime; + +part of '../runtime.dart'; /// A controller of user notifications allow for creating, scheduling, and /// canceling user notifications. diff --git a/carp_mobile_sensing/lib/runtime/util/cron_parser.dart b/carp_mobile_sensing/lib/runtime/util/cron_parser.dart index d2e3ac26..0da95e0b 100644 --- a/carp_mobile_sensing/lib/runtime/util/cron_parser.dart +++ b/carp_mobile_sensing/lib/runtime/util/cron_parser.dart @@ -25,7 +25,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -part of runtime; +part of '../runtime.dart'; abstract class HasNext { /// Find next suitable date From 2145b915bfb9cb66d18290c3ad1b511eed821718 Mon Sep 17 00:00:00 2001 From: bardram Date: Thu, 18 Apr 2024 12:22:15 +0200 Subject: [PATCH 6/6] update of CHANGELOG + release of 1.5.0 --- carp_mobile_sensing/CHANGELOG.md | 1 + carp_mobile_sensing/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/carp_mobile_sensing/CHANGELOG.md b/carp_mobile_sensing/CHANGELOG.md index c45fa2d6..70d18b32 100644 --- a/carp_mobile_sensing/CHANGELOG.md +++ b/carp_mobile_sensing/CHANGELOG.md @@ -4,6 +4,7 @@ * support app-specific methods to create, schedule, and cancel app-specific notifications * asks for permission on Android (Issue [#375](https://github.com/cph-cachet/carp.sensing-flutter/issues/375)) * tapping a notification takes the user to the right task in the app (Issue [#376](https://github.com/cph-cachet/carp.sensing-flutter/issues/376)) +* fix of [#379](https://github.com/cph-cachet/carp.sensing-flutter/issues/379) ## 1.4.7 diff --git a/carp_mobile_sensing/README.md b/carp_mobile_sensing/README.md index 7e8e39ec..cd37a8e9 100644 --- a/carp_mobile_sensing/README.md +++ b/carp_mobile_sensing/README.md @@ -234,7 +234,7 @@ Calling `SmartPhoneClientManager().dispose()` would dispose of the client manage ## Extending CAMS -CAMS is designed to be extended in many ways, including [adding new sampling capabilities](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-new-sampling-capabilities) by implementing a SamplingPackage, [adding a new data management and backend support](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-a-new-data-manager) by creating a DataManager, and [creating data and privacy transformer schemas](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-data-and-privacy-transformers) that can transform CARP data to other formats, including privacy protecting them, by implementing a [TransformerSchema](https://pub.dev/documentation/carp_mobile_sensing/latest/domain/DataTransformerSchema-class.html). +CAMS is designed to be extended in many ways, including [adding new sampling capabilities](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-new-sampling-capabilities) by implementing a Sampling Package, [adding a new data management and backend support](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-a-new-data-manager) by creating a Data Manager, and [creating data and privacy transformer schemas](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-data-and-privacy-transformers) that can transform CARP data to other formats, including privacy protecting them, by implementing a [Transformer Schema](https://pub.dev/documentation/carp_mobile_sensing/latest/domain/DataTransformerSchema-class.html). For example, you can write your own `DataEndPoint` definitions and a corresponding [`DataManager`](https://pub.dev/documentation/carp_mobile_sensing/latest/runtime/DataManager-class.html) class for uploading data to your own data endpoint. See the wiki on how to [add a new data manager](https://github.com/cph-cachet/carp.sensing-flutter/wiki/5.-Extending-CARP-Mobile-Sensing#adding-a-new-data-manager).