diff --git a/modules/ensemble/lib/action/action_invokable.dart b/modules/ensemble/lib/action/action_invokable.dart index d16bc50d0..9eb17431d 100644 --- a/modules/ensemble/lib/action/action_invokable.dart +++ b/modules/ensemble/lib/action/action_invokable.dart @@ -46,6 +46,8 @@ abstract class ActionInvokable with Invokable { ActionType.controlDeviceBackNavigation, ActionType.closeApp, ActionType.getLocation, + ActionType.getMotionData, + ActionType.stopMotionData, ActionType.pickFiles, ActionType.openPlaidLink, ActionType.updateBadgeCount, diff --git a/modules/ensemble/lib/action/getMotionData.dart b/modules/ensemble/lib/action/getMotionData.dart new file mode 100644 index 000000000..2ab31fa71 --- /dev/null +++ b/modules/ensemble/lib/action/getMotionData.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:ensemble/framework/stub/activity_manager.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble/util/utils.dart'; +import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; + +class GetMotionDataAction extends EnsembleAction { + GetMotionDataAction({ + super.initiator, + this.id, + this.onDataReceived, + this.onError, + this.recurring = true, + this.sensors, + this.updateInterval, + }); + + final String? id; + final EnsembleAction? onDataReceived; + final EnsembleAction? onError; + final bool? recurring; + final List? sensors; + final int? updateInterval; // in milliseconds + + factory GetMotionDataAction.fromYaml({ + Invokable? initiator, + Map? payload, + }) { + List? sensors; + + final rawSensors = payload?['options']?['sensors']; + if (rawSensors is List) { + sensors = rawSensors + .map((e) => MotionSensorType.values.firstWhere( + (v) => v.name == e.toString().toLowerCase(), + orElse: () => throw LanguageError( + 'Invalid sensor type: $e', + ), + )) + .toList(); + } + + return GetMotionDataAction( + initiator: initiator, + id: Utils.optionalString(payload?['id']), + onDataReceived: + EnsembleAction.from(payload?['options']?['onDataReceived']), + onError: EnsembleAction.from(payload?['options']?['onError']), + recurring: + Utils.getBool(payload?['options']?['recurring'], fallback: true), + sensors: sensors, + updateInterval: Utils.optionalInt( + payload?['options']?['updateInterval'], + ), + ); + } + + @override + Future execute( + BuildContext context, + ScopeManager scopeManager, + ) async { + if (onDataReceived == null) { + throw LanguageError( + '${ActionType.getMotionData.name} requires onDataReceived callback', + ); + } + + try { + if (recurring == true) { + final subscription = GetIt.I() + .startMotionStream( + sensors: sensors, + updateInterval: updateInterval != null + ? Duration(milliseconds: updateInterval!) + : null, + ) + .listen( + (MotionData data) { + ScreenController().executeActionWithScope( + context, + scopeManager, + onDataReceived!, + event: EnsembleEvent( + initiator, + data: data.toJson(), + ), + ); + }, + onError: (error) { + if (onError != null) { + ScreenController().executeActionWithScope( + context, + scopeManager, + onError!, + event: EnsembleEvent( + initiator, + error: error.toString(), + ), + ); + } + }, + ); + + scopeManager.addMotionListener(subscription, id: id); + } else { + final data = + await GetIt.I().getMotionData(sensors: sensors); + + if (data != null) { + ScreenController().executeActionWithScope( + context, + scopeManager, + onDataReceived!, + event: EnsembleEvent( + initiator, + data: data.toJson(), + ), + ); + } else if (onError != null) { + ScreenController().executeActionWithScope( + context, + scopeManager, + onError!, + event: EnsembleEvent( + initiator, + error: 'noData', + ), + ); + } + } + } catch (e) { + if (onError != null) { + ScreenController().executeActionWithScope( + context, + scopeManager, + onError!, + event: EnsembleEvent( + initiator, + error: e.toString(), + ), + ); + } else { + rethrow; + } + } + } +} + +class StopMotionDataAction extends EnsembleAction { + StopMotionDataAction({this.id}); + final String? id; + factory StopMotionDataAction.fromYaml({Map? payload}) { + return StopMotionDataAction( + id: Utils.optionalString(payload?['id']), + ); + } + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + scopeManager.stopMotionListener(id); + } +} diff --git a/modules/ensemble/lib/framework/action.dart b/modules/ensemble/lib/framework/action.dart index 45475125c..e018a27d8 100644 --- a/modules/ensemble/lib/framework/action.dart +++ b/modules/ensemble/lib/framework/action.dart @@ -33,6 +33,7 @@ import 'package:ensemble/action/take_screenshot.dart'; import 'package:ensemble/action/disable_hardware_navigation.dart'; import 'package:ensemble/action/close_app.dart'; import 'package:ensemble/action/getLocation.dart'; +import 'package:ensemble/action/getMotionData.dart'; import 'package:ensemble/action/wakelock_action.dart'; import 'package:ensemble/framework/apiproviders/api_provider.dart'; import 'package:ensemble/framework/apiproviders/http_api_provider.dart'; @@ -1051,6 +1052,9 @@ enum ActionType { // Stripe actions initializeStripe, showPaymentSheet, + // Activity actions + getMotionData, + stopMotionData, } /// payload representing an Action to do (navigateToScreen, InvokeAPI, ..) @@ -1304,6 +1308,11 @@ abstract class EnsembleAction { } else if (actionType == ActionType.showPaymentSheet) { return ShowPaymentSheetAction.fromYaml( initiator: initiator, payload: payload); + } else if (actionType == ActionType.getMotionData) { + return GetMotionDataAction.fromYaml( + initiator: initiator, payload: payload); + } else if (actionType == ActionType.stopMotionData) { + return StopMotionDataAction.fromYaml(payload: payload); } else { throw LanguageError("Invalid action.", recovery: "Make sure to use one of Ensemble-provided actions."); diff --git a/modules/ensemble/lib/framework/scope.dart b/modules/ensemble/lib/framework/scope.dart index 6b570b2a9..23b1dca65 100644 --- a/modules/ensemble/lib/framework/scope.dart +++ b/modules/ensemble/lib/framework/scope.dart @@ -12,6 +12,7 @@ import 'package:ensemble/framework/ensemble_widget.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; import 'package:ensemble/framework/stub/location_manager.dart'; +import 'package:ensemble/framework/stub/activity_manager.dart'; import 'package:ensemble/framework/theme_manager.dart'; import 'package:ensemble/framework/view/data_scope_widget.dart'; import 'package:ensemble/framework/view/page.dart'; @@ -81,6 +82,8 @@ class ScopeManager extends IsScopeManager with ViewBuilder, PageBindingManager { // cancel the screen's location listener pageData.locationListener?.cancel(); + // cancel the screen's motion listener + pageData.motionListener?.cancel(); SocketService().dispose(); } @@ -93,6 +96,26 @@ class ScopeManager extends IsScopeManager with ViewBuilder, PageBindingManager { pageData.locationListener = streamSubscription; } + /// only 1 motion listener per screen + void addMotionListener(StreamSubscription streamSubscription, + {String? id}) { + // first cancel the previous one + pageData.motionListener?.cancel(); + pageData.motionListener = streamSubscription; + pageData.motionListenerId = id; + } + + /// stop the motion listener + void stopMotionListener([String? id]) { + // if id is provided, only cancel if it matches + if (id != null && pageData.motionListenerId != id) { + return; + } + pageData.motionListener?.cancel(); + pageData.motionListener = null; + pageData.motionListenerId = null; + } + // add repeating timer so we can manage it later. void addTimer(StartTimerAction timerAction, Timer timer) { EnsembleTimer newTimer = EnsembleTimer(timer, id: timerAction.id); @@ -708,6 +731,10 @@ class PageData { /// 1 recurring location listener per page StreamSubscription? locationListener; + /// 1 recurring motion listener per page + StreamSubscription? motionListener; + String? motionListenerId; + // list of all opened Dialogs' contexts final List openedDialogs = []; diff --git a/modules/ensemble/lib/framework/stub/activity_manager.dart b/modules/ensemble/lib/framework/stub/activity_manager.dart new file mode 100644 index 000000000..2a8d216ec --- /dev/null +++ b/modules/ensemble/lib/framework/stub/activity_manager.dart @@ -0,0 +1,126 @@ +import 'dart:async'; +import 'package:ensemble/framework/error_handling.dart'; + +abstract class ActivityManager { + // Motion sensor methods + Stream startMotionStream({ + List? sensors, + Duration? updateInterval, + }); + void stopMotionStream(); + Future getMotionData({List? sensors}); +} + +class ActivityManagerStub extends ActivityManager { + @override + Stream startMotionStream({ + List? sensors, + Duration? updateInterval, + }) { + throw ConfigError( + "Activity Manager is not enabled. Please review the Ensemble documentation."); + } + + @override + void stopMotionStream() { + throw ConfigError( + "Activity Manager is not enabled. Please review the Ensemble documentation."); + } + + @override + Future getMotionData({List? sensors}) { + throw ConfigError( + "Activity Manager is not enabled. Please review the Ensemble documentation."); + } +} + +class MotionData { + MotionData({ + this.accelerometer, + this.gyroscope, + this.magnetometer, + this.pedometer, + required this.timestamp, + }); + + final AccelerometerData? accelerometer; + final GyroscopeData? gyroscope; + final MagnetometerData? magnetometer; + final PedometerData? pedometer; + final DateTime timestamp; + + Map toJson() { + return { + 'accelerometer': accelerometer?.toJson(), + 'gyroscope': gyroscope?.toJson(), + 'magnetometer': magnetometer?.toJson(), + 'pedometer': pedometer?.toJson(), + 'timestamp': timestamp.toIso8601String(), + }; + } +} + +class AccelerometerData { + AccelerometerData({required this.x, required this.y, required this.z}); + + final double x; + final double y; + final double z; + + Map toJson() { + return {'x': x, 'y': y, 'z': z}; + } +} + +class GyroscopeData { + GyroscopeData({required this.x, required this.y, required this.z}); + + final double x; + final double y; + final double z; + + Map toJson() { + return {'x': x, 'y': y, 'z': z}; + } +} + +class MagnetometerData { + MagnetometerData({required this.x, required this.y, required this.z}); + + final double x; + final double y; + final double z; + + Map toJson() { + return {'x': x, 'y': y, 'z': z}; + } +} + +class PedometerData { + PedometerData( + {required this.steps, + required this.stepsOnStart, + required this.distanceMeters, + required this.status}); + + final int steps; + final int stepsOnStart; + final double distanceMeters; + final String status; + + Map toJson() { + return { + 'steps': steps, + 'stepsOnStart': stepsOnStart, + 'distanceMeters': distanceMeters, + 'status': status + }; + } +} + +enum MotionSensorType { + accelerometer, + gyroscope, + magnetometer, + pedometer, +} diff --git a/modules/ensemble/lib/module/activity_module.dart b/modules/ensemble/lib/module/activity_module.dart new file mode 100644 index 000000000..62dd8e558 --- /dev/null +++ b/modules/ensemble/lib/module/activity_module.dart @@ -0,0 +1,10 @@ +import 'package:ensemble/framework/stub/activity_manager.dart'; +import 'package:get_it/get_it.dart'; + +abstract class ActivityModule {} + +class ActivityModuleStub implements ActivityModule { + ActivityModuleStub() { + GetIt.I.registerSingleton(ActivityManagerStub()); + } +} diff --git a/modules/ensemble_activity/README.md b/modules/ensemble_activity/README.md new file mode 100644 index 000000000..46cb6505f --- /dev/null +++ b/modules/ensemble_activity/README.md @@ -0,0 +1,183 @@ +# Ensemble Activity + +The Ensemble Activity module provides access to device motion and activity sensors for tracking user movement, orientation, and walking activity. + +It supports: +- Real-time streaming +- One-time motion reads +- Selective sensor subscription +- Automatic lifecycle management + +## Features + +### Motion Sensors + +Supported sensors: +- Accelerometer — device acceleration on X/Y/Z axes +- Gyroscope — rotation rate on X/Y/Z axes +- Magnetometer — magnetic field / compass direction +- Pedometer + - Step count since stream start + - Walking status (walking, stopped, etc.) + - Estimated distance in meters + +### Streaming Capabilities + +- Subscribe to one or multiple sensors +- Merged motion stream for all selected sensors +- Broadcast stream (multiple listeners supported) +- Explicit start / stop lifecycle +- Automatic cleanup when stopped or cancelled + +### Permissions + +Activity Recognition permission is automatically requested when the pedometer sensor is included. If permission is denied, the stream emits an error event. + +## Motion Data Model + +Each emitted event contains a MotionData object: + +```json +{ + "accelerometer": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "gyroscope": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "magnetometer": { "x": 0.0, "y": 0.0, "z": 0.0 }, + "pedometer": { + "steps": 120, + "status": "walking", + "stepsOnStart": 3450, + "distanceMeters": 90.0 + }, + "timestamp": "2026-02-07T10:30:00.000Z" +} +``` + +Notes: +- Only requested sensors are populated +- Non-requested sensors may be null + +## Available Actions + +### getMotionData + +Starts a motion stream or performs a single motion read. + +**Parameters** + +| Field | Type | Description | +|-------|------|-------------| +| id | string (optional) | Identifier for the motion stream | +| options.sensors | string[] | List of sensors to subscribe to | +| options.updateInterval | number | Update interval in milliseconds | +| options.recurring | boolean (optional) | Defaults to true | +| options.onDataReceived | action | Executed when motion data is emitted | +| options.onError | action | Executed when an error occurs | + +**Sensor Values** + +Valid sensor names: +- accelerometer +- gyroscope +- magnetometer +- pedometer + +**Behavior** + +- If a stream with the same id already exists, the existing stream is reused +- If recurring is set to false, returns one motion event and stops automatically +- Step counter resets when stream starts + +**Examples** + +Streaming Motion Data: + +```yaml +getMotionData: + id: motion_stream + options: + sensors: + - accelerometer + - gyroscope + - magnetometer + updateInterval: 1000 + onDataReceived: + executeCode: + body: |- + console.log('Motion data:', event.data); + onError: + executeCode: + body: |- + console.log('Motion error:', event.error); +``` + +Pedometer Tracking: + +```yaml +getMotionData: + id: pedometer_stream + options: + sensors: + - pedometer + updateInterval: 1000 + onDataReceived: + executeCode: + body: |- + var p = event.data && event.data.pedometer; + if (p) { + console.log('Steps:', p.steps, 'Distance:', p.distanceMeters); + } +``` + +One-Time Motion Read: + +```yaml +getMotionData: + options: + recurring: false + sensors: + - accelerometer + onDataReceived: + executeCode: + body: |- + console.log('Single read:', event.data); +``` + +### stopMotionData + +Stops an active motion stream and releases sensor subscriptions. + +**Parameters** + +| Field | Type | Description | +|-------|------|-------------| +| id | string (optional) | ID of the stream to stop | + +**Behavior** + +- Cancels active sensor subscriptions +- Stops pedometer tracking +- Clears cached motion values +- Fully closes the motion stream + +**Example** + +```yaml +stopMotionData: + id: motion_stream +``` + +## Lifecycle Notes + +- Multiple sensors are merged into one stream +- A new MotionData event is emitted whenever any sensor updates +- Calling stopMotionData fully resets motion state +- UI cancellation automatically stops tracking + +## Platform Notes + +**Android** +- Requires ACTIVITY_RECOGNITION permission for pedometer + +**iOS** +- Uses Core Motion pedometer APIs +- Distance estimated using approximately 0.75 m step length diff --git a/modules/ensemble_activity/lib/activity_manager.dart b/modules/ensemble_activity/lib/activity_manager.dart new file mode 100644 index 000000000..dc4e5f5e7 --- /dev/null +++ b/modules/ensemble_activity/lib/activity_manager.dart @@ -0,0 +1,239 @@ +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:ensemble/framework/stub/activity_manager.dart'; +import 'package:pedometer/pedometer.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:sensors_plus/sensors_plus.dart'; + +class ActivityManagerImpl extends ActivityManager { + // Subscriptions + StreamSubscription? _accelerometerSubscription; + StreamSubscription? _gyroscopeSubscription; + StreamSubscription? _magnetometerSubscription; + StreamSubscription? _pedometerSubscription; + + // Controller + StreamController? _motionController; + bool _running = false; + + // Latest sensor values + AccelerometerData? _latestAccelerometer; + GyroscopeData? _latestGyroscope; + MagnetometerData? _latestMagnetometer; + PedometerData? _latestPedometer; + + // Pedometer state + int? _stepsOnStart; + int _stepsSinceStart = 0; + String _pedestrianStatus = 'initializing'; + + static const double stepLengthMeters = 0.75; + + // Permissions + Future requestActivityPermission() async { + final status = await Permission.activityRecognition.status; + if (status.isGranted) return true; + + final result = await Permission.activityRecognition.request(); + return result.isGranted; + } + + // Public API + @override + Stream startMotionStream({ + List? sensors, + Duration? updateInterval, + }) { + if (_running && _motionController != null) { + return _motionController!.stream; + } + + _running = true; + + final activeSensors = (sensors == null || sensors.isEmpty) + ? MotionSensorType.values + : sensors; + + print( + 'Starting motion stream with sensors: $activeSensors and updateInterval: $updateInterval'); + + _motionController = StreamController.broadcast( + onCancel: stopMotionStream, + ); + + _startSensors(activeSensors); + + return _motionController!.stream; + } + + @override + void stopMotionStream() { + if (!_running) return; + + _running = false; + + _accelerometerSubscription?.cancel(); + _gyroscopeSubscription?.cancel(); + _magnetometerSubscription?.cancel(); + _pedometerSubscription?.cancel(); + + _accelerometerSubscription = null; + _gyroscopeSubscription = null; + _magnetometerSubscription = null; + _pedometerSubscription = null; + + _motionController?.close(); + _motionController = null; + + _resetState(); + } + + @override + Future getMotionData({ + List? sensors, + }) async { + try { + final stream = startMotionStream(sensors: sensors); + final data = await stream.first; + stopMotionStream(); + return data; + } catch (_) { + stopMotionStream(); + return null; + } + } + + void dispose() { + stopMotionStream(); + } + + // Internal helpers + void _resetState() { + _latestAccelerometer = null; + _latestGyroscope = null; + _latestMagnetometer = null; + _latestPedometer = null; + + _stepsOnStart = null; + _stepsSinceStart = 0; + _pedestrianStatus = 'initializing'; + } + + void _startSensors(List sensors) async { + if (sensors.contains(MotionSensorType.pedometer)) { + final ok = await requestActivityPermission(); + if (!ok) { + _motionController?.addError( + Exception('Activity recognition permission denied'), + ); + return; + } + _startPedometer(); + } + + if (sensors.contains(MotionSensorType.accelerometer)) { + _accelerometerSubscription = accelerometerEvents.listen(_onAccelerometer); + } + + if (sensors.contains(MotionSensorType.gyroscope)) { + _gyroscopeSubscription = gyroscopeEvents.listen(_onGyroscope); + } + + if (sensors.contains(MotionSensorType.magnetometer)) { + _magnetometerSubscription = magnetometerEvents.listen(_onMagnetometer); + } + } + + // Sensor handlers + void _onAccelerometer(AccelerometerEvent event) { + if (!_running) return; + + _latestAccelerometer = AccelerometerData( + x: event.x, + y: event.y, + z: event.z, + ); + + _emit(); + } + + void _onGyroscope(GyroscopeEvent event) { + if (!_running) return; + + _latestGyroscope = GyroscopeData( + x: event.x, + y: event.y, + z: event.z, + ); + + _emit(); + } + + void _onMagnetometer(MagnetometerEvent event) { + if (!_running) return; + + _latestMagnetometer = MagnetometerData( + x: event.x, + y: event.y, + z: event.z, + ); + + _emit(); + } + + void _startPedometer() { + _stepsOnStart = null; + _stepsSinceStart = 0; + _pedestrianStatus = 'initializing'; + + _latestPedometer = PedometerData( + steps: 0, + stepsOnStart: 0, + distanceMeters: 0, + status: 'initializing', + ); + + _pedometerSubscription = StreamGroup.merge([ + Pedometer.stepCountStream.map((event) { + _stepsOnStart ??= event.steps; + _stepsSinceStart = event.steps - (_stepsOnStart ?? event.steps); + }), + Pedometer.pedestrianStatusStream.map((event) { + _pedestrianStatus = event.status; + }), + ]).listen((_) { + if (!_running) return; + + _latestPedometer = PedometerData( + steps: _stepsSinceStart, + stepsOnStart: _stepsOnStart ?? 0, + distanceMeters: _stepsSinceStart * stepLengthMeters, + status: _pedestrianStatus, + ); + + _emit(); + }); + } + + void _emit() { + if (!_running || _motionController == null) return; + + if (_latestAccelerometer == null && + _latestGyroscope == null && + _latestMagnetometer == null && + _latestPedometer == null) { + return; + } + + _motionController!.add( + MotionData( + accelerometer: _latestAccelerometer, + gyroscope: _latestGyroscope, + magnetometer: _latestMagnetometer, + pedometer: _latestPedometer, + timestamp: DateTime.now(), + ), + ); + } +} diff --git a/modules/ensemble_activity/lib/activity_module.dart b/modules/ensemble_activity/lib/activity_module.dart new file mode 100644 index 000000000..715dccd5c --- /dev/null +++ b/modules/ensemble_activity/lib/activity_module.dart @@ -0,0 +1,11 @@ +import 'package:ensemble/framework/stub/activity_manager.dart'; +import 'package:ensemble/module/activity_module.dart'; +import 'package:get_it/get_it.dart'; +import 'activity_manager.dart'; + +class ActivityModuleImpl implements ActivityModule { + ActivityModuleImpl() { + GetIt.I.registerSingleton(ActivityManagerImpl()); + } +} + diff --git a/modules/ensemble_activity/pubspec.yaml b/modules/ensemble_activity/pubspec.yaml new file mode 100644 index 000000000..7b07a47e4 --- /dev/null +++ b/modules/ensemble_activity/pubspec.yaml @@ -0,0 +1,68 @@ +name: ensemble_activity +description: Activity module for Ensemble framework +version: 0.0.1 +homepage: https://github.com/EnsembleUI/ensemble/modules/activity +publish_to: none + +environment: + sdk: ">=3.5.0" + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + + ensemble: + git: + url: https://github.com/EnsembleUI/ensemble.git + ref: ensemble-v1.2.28 + path: modules/ensemble + + ensemble_ts_interpreter: ^1.0.7 + + get_it: ^8.0.0 + sensors_plus: ^7.0.0 + permission_handler: ^12.0.1 + pedometer: ^4.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Regular.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/starter/lib/generated/ensemble_modules.dart b/starter/lib/generated/ensemble_modules.dart index 2935a6c54..1044e4a18 100644 --- a/starter/lib/generated/ensemble_modules.dart +++ b/starter/lib/generated/ensemble_modules.dart @@ -14,6 +14,7 @@ import 'package:ensemble/framework/stub/contacts_manager.dart'; import 'package:ensemble/framework/stub/plaid_link_manager.dart'; import 'package:ensemble/module/auth_module.dart'; import 'package:ensemble/module/location_module.dart'; +import 'package:ensemble/module/activity_module.dart'; import 'package:ensemble/framework/stub/moengage_manager.dart'; import 'package:ensemble/module/stripe_module.dart'; // import 'package:ensemble_stripe/ensemble_stripe.dart'; @@ -57,6 +58,9 @@ import 'package:get_it/get_it.dart'; // Uncomment to enable location services // import 'package:ensemble_location/location_manager.dart'; +// Uncomment to enable activity services +// import 'package:ensemble_activity/activity_module.dart'; + // Uncomment to enable deeplink services // import 'package:ensemble_deeplink/deferred_link_manager.dart'; @@ -81,6 +85,7 @@ class EnsembleModules { static const useContacts = false; static const useConnect = false; static const useLocation = false; + static const useActivity = false; static const useDeeplink = false; static const useFirebaseAnalytics = false; static const useNotifications = false; @@ -176,6 +181,13 @@ class EnsembleModules { GetIt.I.registerSingleton(LocationModuleStub()); } + if (useActivity) { + // Uncomment to enable ensemble_activity service + // GetIt.I.registerSingleton(ActivityModuleImpl()); + } else { + GetIt.I.registerSingleton(ActivityModuleStub()); + } + if (useDeeplink) { // Uncomment to enable ensemble_deeplink service // GetIt.I.registerSingleton(DeferredLinkManagerImpl()); diff --git a/starter/pubspec.yaml b/starter/pubspec.yaml index 6c6bde3a8..252c97fd9 100644 --- a/starter/pubspec.yaml +++ b/starter/pubspec.yaml @@ -94,6 +94,13 @@ dependencies: # ref: main # path: modules/location + # Uncomment to enable activity module + # ensemble_activity: + # git: + # url: https://github.com/EnsembleUI/ensemble.git + # ref: main + # path: modules/ensemble_activity + # Uncomment to enable deeplink module # ensemble_deeplink: # git: diff --git a/starter/scripts/modules/enable_activity.dart b/starter/scripts/modules/enable_activity.dart new file mode 100644 index 000000000..ab845e2d9 --- /dev/null +++ b/starter/scripts/modules/enable_activity.dart @@ -0,0 +1,83 @@ +import 'dart:io'; + +import '../utils.dart'; + +void main(List arguments) async { + List platforms = getPlatforms(arguments); + String? ensembleVersion = getArgumentValue(arguments, 'ensemble_version'); + + final statements = { + 'moduleStatements': [ + "import 'package:ensemble_activity/ensemble_activity.dart';", + 'GetIt.I.registerSingleton(ActivityManagerImpl());', + ], + 'useStatements': [ + 'static const useActivity = true;', + ], + }; + + final pubspecDependencies = [ + { + 'statement': ''' +ensemble_activity: + git: + url: https://github.com/EnsembleUI/ensemble.git + ref: ${await packageVersion(version: ensembleVersion)} + path: modules/ensemble_activity''', + 'regex': + r'#\s*ensemble_activity:\s*\n\s*#\s*git:\s*\n\s*#\s*url:\s*https:\/\/github\.com\/EnsembleUI\/ensemble\.git\s*\n\s*#\s*ref:\s*main\s*\n\s*#\s*path:\s*modules\/ensemble_activity', + } + ]; + + final androidPermissions = [ + // Required for activity recognition (Android 10+) + '', + ]; + + final iOSPermissions = [ + { + 'key': 'motionDescription', + 'value': 'NSMotionUsageDescription', + }, + ]; + final iOSAdditionalSettings = [ + { + // Required if you want motion updates while app is in background + 'key': 'UIBackgroundModes', + 'value': ['motion'], + 'isArray': true, + }, + ]; + + try { + // Update ensemble_modules.dart + updateEnsembleModules( + statements['moduleStatements'], + statements['useStatements'], + ); + + // Update pubspec.yaml + updatePubspec(pubspecDependencies); + + // Android permissions + if (platforms.contains('android')) { + updateAndroidPermissions(permissions: androidPermissions); + } + + // iOS permissions (no background modes) + if (platforms.contains('ios')) { + updateIOSPermissions( + iOSPermissions, + arguments, + additionalSettings: iOSAdditionalSettings, + ); + } + + print( + 'Activity recognition module enabled successfully for ${platforms.join(', ')}! 🚶'); + exit(0); + } catch (e) { + print('Starter Error: $e'); + exit(1); + } +} diff --git a/starter/src/modules_scripts.ts b/starter/src/modules_scripts.ts index 3a1b2203a..419898df5 100644 --- a/starter/src/modules_scripts.ts +++ b/starter/src/modules_scripts.ts @@ -241,6 +241,18 @@ export const modules: Script[] = [ }, ], }, + { + name: 'activity', + path: 'scripts/modules/enable_activity.dart', + parameters: [ + { + key: 'motionDescription', + question: 'Please provide a description for motion & activity usage: ', + platform: ['ios'], + type: 'text', + }, + ], + }, { name: 'bluetooth', path: 'scripts/modules/enable_bluetooth.dart',