diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5de857a..72c9895 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
/dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ FEF6B413DA9FC937656B660D /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -362,13 +471,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F2VMBPUNG6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.front;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -378,6 +488,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -395,6 +506,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = E805B90C1540E73EE0890817 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -410,6 +522,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -541,13 +654,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F2VMBPUNG6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.front;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -563,13 +677,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = F2VMBPUNG6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.front;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
index 1d526a1..21a3cc1 100644
--- a/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -4,4 +4,7 @@
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 7788352..010caf8 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
@@ -24,6 +26,10 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NSCalendarsFullAccessUsageDescription
+ Access most functions for calendar viewing.
+ UIApplicationSupportsIndirectInputEvents
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -41,9 +47,5 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart
index c788fa8..86b3ded 100644
--- a/lib/screens/landing_screen.dart
+++ b/lib/screens/landing_screen.dart
@@ -7,6 +7,7 @@ import '../services/websocket_service.dart';
import '../services/phone_audio_service.dart';
import 'login_screen.dart';
import 'register_screen.dart';
+import '../services/calendar_service.dart';
import 'dart:async';
/// Landing screen of the app. Manages BLE glasses connection,
@@ -32,6 +33,7 @@ class _LandingScreenState extends State {
late final Lc3Decoder _decoder;
late final WebsocketService _ws;
late final AudioPipeline _audioPipeline;
+ late final CalendarService _calendarService;
late final PhoneAudioService _phoneAudio;
bool _usePhoneMic = false;
@@ -48,6 +50,7 @@ class _LandingScreenState extends State {
_manager = widget.manager ?? G1Manager();
_decoder = widget.decoder ?? Lc3Decoder();
_ws = widget.ws ?? WebsocketService();
+ _calendarService = CalendarService();
_audioPipeline = widget.audioPipeline ??
AudioPipeline(
_manager,
@@ -275,6 +278,26 @@ class _LandingScreenState extends State {
Expanded(
child: GlassesConnection(
manager: _manager,
+ onRecordToggle: () async {
+ if (!_manager.transcription.isActive.value) {
+ final granted =
+ await _calendarService.requestPermission();
+ if (granted) {
+ final events = await _calendarService
+ .getUpcomingEvents();
+ final activeEvent = _calendarService
+ .selectActiveContext(events);
+ if (activeEvent != null) {
+ final payload = _calendarService
+ .buildCalendarPayload(activeEvent);
+ _ws.sendCalendarContext(payload);
+ }
+ }
+ await _startTranscription();
+ } else {
+ await _stopTranscription();
+ }
+ },
),
),
diff --git a/lib/services/calendar_service.dart b/lib/services/calendar_service.dart
new file mode 100644
index 0000000..2eabc15
--- /dev/null
+++ b/lib/services/calendar_service.dart
@@ -0,0 +1,113 @@
+import 'package:device_calendar/device_calendar.dart';
+
+class CalendarService {
+ final DeviceCalendarPlugin _calendarPlugin = DeviceCalendarPlugin();
+
+ //Requests calendar permission
+ Future requestPermission() async {
+ var permissionsGranted = await _calendarPlugin.requestPermissions();
+ if (permissionsGranted.isSuccess && permissionsGranted.data == true) {
+ return true;
+ // Permission granted, you can now access the calendar
+ } else {
+ return false;
+ // Permission denied, handle accordingly
+ }
+ }
+
+ //Searches for upcoming events in the next 7 days
+ Future> getUpcomingEvents() async {
+ var calendarResult = await _calendarPlugin.retrieveCalendars();
+ if (calendarResult.isSuccess && calendarResult.data != null) {
+ List calendars = calendarResult.data!;
+ List events = [];
+
+ DateTime startDate = DateTime.now();
+ DateTime endDate = startDate.add(const Duration(days: 7));
+
+ for (var calendar in calendars) {
+ var eventResult = await _calendarPlugin.retrieveEvents(
+ calendar.id!,
+ RetrieveEventsParams(startDate: startDate, endDate: endDate),
+ );
+
+ if (eventResult.isSuccess && eventResult.data != null) {
+ List calendarEvents = eventResult.data!;
+ for (var event in calendarEvents) {
+ if (event.start != null && event.end != null) {
+ events.add(
+ CalendarEventModel(
+ title: event.title ?? 'No Title',
+ description: event.description,
+ start: event.start!,
+ end: event.end!,
+ ),
+ );
+ }
+ }
+ }
+ }
+ events.sort((a, b) => a.start.compareTo(b.start));
+ return events;
+ }
+ return [];
+ }
+
+ //Selects the active or upcoming event
+ CalendarEventModel? selectActiveContext(List events) {
+ DateTime now = DateTime.now();
+ //Event is happening now
+ for (var event in events) {
+ if (event.start.isBefore(now) && event.end.isAfter(now)) {
+ return event;
+ }
+ }
+ //Upcoming event
+ for (var event in events) {
+ if (event.start.isAfter(now)) {
+ return event;
+ }
+ }
+ //No active or upcoming events
+ return null;
+ }
+
+ //Builds the payload to send to backend
+ Map buildCalendarPayload(CalendarEventModel? event) {
+ if (event == null) {
+ return {
+ "type": "calendar_context",
+ "data": {
+ "title": "General conversation",
+ "description": null,
+ "start": null,
+ "end": null
+ }
+ };
+ }
+ return {
+ "type": "calendar_context",
+ "data": {
+ "title": event.title,
+ "description": event.description,
+ "start": event.start.toIso8601String(),
+ "end": event.end.toIso8601String()
+ }
+ };
+ }
+}
+
+// Model to represent calendar events in a simplified way for our application
+class CalendarEventModel {
+ final String title;
+ final String? description;
+ final DateTime start;
+ final DateTime end;
+
+ CalendarEventModel({
+ required this.title,
+ required this.description,
+ required this.start,
+ required this.end,
+ });
+}
diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart
index 52adcfb..aa61d00 100644
--- a/lib/services/websocket_service.dart
+++ b/lib/services/websocket_service.dart
@@ -130,6 +130,12 @@ class WebsocketService {
}
}
+ void sendCalendarContext(Map payload) {
+ if (connected.value) {
+ _audioChannel?.sink.add(jsonEncode(payload));
+ }
+ }
+
/// Tell the backend to stop expecting audio data.
Future stopAudioStream() async {
if (connected.value) {
diff --git a/pubspec.lock b/pubspec.lock
index 1ecbb85..aa8ce67 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -97,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
+ device_calendar:
+ dependency: "direct main"
+ description:
+ name: device_calendar
+ sha256: "683fb93ec302b6a65c0ce57df40ff9dcc2404f59c67a2f8b93e59318c8a0a225"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.3.3"
even_realities_g1:
dependency: "direct main"
description:
@@ -500,6 +508,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
stack_trace:
dependency: transitive
description:
@@ -548,6 +564,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.7"
+ timezone:
+ dependency: transitive
+ description:
+ name: timezone
+ sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.4"
typed_data:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 87ed094..b2a8782 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -33,6 +33,7 @@ dependencies:
cupertino_icons: ^1.0.8
# Local path dependency
+ device_calendar: ^4.3.3
even_realities_g1:
path: packages/even_realities_g1
flutter:
diff --git a/test/services/calendar_service_test.dart b/test/services/calendar_service_test.dart
new file mode 100644
index 0000000..3b6252f
--- /dev/null
+++ b/test/services/calendar_service_test.dart
@@ -0,0 +1,77 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:front/services/calendar_service.dart';
+
+void main() {
+ group('CalendarService.selectActiveContext', () {
+ final service = CalendarService();
+
+ test('returns null when event list is empty', () {
+ final result = service.selectActiveContext([]);
+
+ expect(result, isNull);
+ });
+ test('return active event when one is happening now', () {
+ final now = DateTime.now();
+ final events = [
+ CalendarEventModel(
+ title: 'Business meeting',
+ description: 'Discussing quarterly results',
+ start: now.subtract(const Duration(minutes: 30)),
+ end: now.add(const Duration(minutes: 30))),
+ ];
+ final result = service.selectActiveContext(events);
+
+ expect(result, isNotNull);
+ expect(result!.title, 'Business meeting');
+ });
+ test('return upcoming event when no event is active', () {
+ final now = DateTime.now();
+ final events = [
+ CalendarEventModel(
+ title: 'Project deadline',
+ description: 'Submit final report',
+ start: now.add(const Duration(hours: 1)),
+ end: now.add(const Duration(hours: 2))),
+ CalendarEventModel(
+ title: 'Later meeting',
+ description: 'Retrospective',
+ start: now.add(const Duration(hours: 3)),
+ end: now.add(const Duration(hours: 4))),
+ ];
+ final result = service.selectActiveContext(events);
+
+ expect(result, isNotNull);
+ expect(result!.title, 'Project deadline');
+ });
+ });
+
+ group('CalendarService.buildCalendarPayload', () {
+ final service = CalendarService();
+
+ test('Build payload from active event', () {
+ final event = CalendarEventModel(
+ title: 'Business meeting',
+ description: 'Discussing quarterly results',
+ start: DateTime.parse('2025-06-01T10:00:00Z'),
+ end: DateTime.parse('2025-06-01T11:00:00Z'));
+ final payload = service.buildCalendarPayload(event);
+
+ expect(payload['type'], 'calendar_context');
+ expect(payload['data']['title'], 'Business meeting');
+ expect(payload['data']['description'], 'Discussing quarterly results');
+ expect(payload['data']['start'], event.start.toIso8601String());
+ expect(payload['data']['end'], event.end.toIso8601String());
+ });
+ test('Build payload with null description', () {
+ final event = CalendarEventModel(
+ title: 'Quick meeting',
+ description: null,
+ start: DateTime.parse('2025-06-01T12:00:00Z'),
+ end: DateTime.parse('2025-06-01T12:30:00Z'));
+ final payload = service.buildCalendarPayload(event);
+
+ expect(payload['data']['title'], 'Quick meeting');
+ expect(payload['data']['description'], isNull);
+ });
+ });
+}