Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/ensemble/lib/action/action_invokable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
168 changes: 168 additions & 0 deletions modules/ensemble/lib/action/getMotionData.dart
Original file line number Diff line number Diff line change
@@ -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<MotionSensorType>? sensors;
final int? updateInterval; // in milliseconds

factory GetMotionDataAction.fromYaml({
Invokable? initiator,
Map? payload,
}) {
List<MotionSensorType>? 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<void> 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<ActivityManager>()
.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<ActivityManager>().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<void> execute(BuildContext context, ScopeManager scopeManager) async {
scopeManager.stopMotionListener(id);
}
}
9 changes: 9 additions & 0 deletions modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1051,6 +1052,9 @@ enum ActionType {
// Stripe actions
initializeStripe,
showPaymentSheet,
// Activity actions
getMotionData,
stopMotionData,
}

/// payload representing an Action to do (navigateToScreen, InvokeAPI, ..)
Expand Down Expand Up @@ -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.");
Expand Down
27 changes: 27 additions & 0 deletions modules/ensemble/lib/framework/scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
Expand All @@ -93,6 +96,26 @@ class ScopeManager extends IsScopeManager with ViewBuilder, PageBindingManager {
pageData.locationListener = streamSubscription;
}

/// only 1 motion listener per screen
void addMotionListener(StreamSubscription<MotionData> 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);
Expand Down Expand Up @@ -708,6 +731,10 @@ class PageData {
/// 1 recurring location listener per page
StreamSubscription<LocationData>? locationListener;

/// 1 recurring motion listener per page
StreamSubscription<MotionData>? motionListener;
String? motionListenerId;

// list of all opened Dialogs' contexts
final List<BuildContext> openedDialogs = [];

Expand Down
126 changes: 126 additions & 0 deletions modules/ensemble/lib/framework/stub/activity_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'dart:async';
import 'package:ensemble/framework/error_handling.dart';

abstract class ActivityManager {
// Motion sensor methods
Stream<MotionData> startMotionStream({
List<MotionSensorType>? sensors,
Duration? updateInterval,
});
void stopMotionStream();
Future<MotionData?> getMotionData({List<MotionSensorType>? sensors});
}

class ActivityManagerStub extends ActivityManager {
@override
Stream<MotionData> startMotionStream({
List<MotionSensorType>? 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<MotionData?> getMotionData({List<MotionSensorType>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'steps': steps,
'stepsOnStart': stepsOnStart,
'distanceMeters': distanceMeters,
'status': status
};
}
}

enum MotionSensorType {
accelerometer,
gyroscope,
magnetometer,
pedometer,
}
Loading