diff --git a/walkguide-mobile/walkguide_app/RUN_COMMAND.md b/walkguide-mobile/walkguide_app/RUN_COMMAND.md
new file mode 100644
index 0000000..86ca289
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/RUN_COMMAND.md
@@ -0,0 +1,6 @@
+D:\Tools\Flutter\flutter\bin\flutter.bat run -d chrome
+D:\Tools\Flutter\flutter\bin\flutter.bat run -d chrome --web-port 49727
+
+Clean Cache:
+cd "D:\CodeSpace\Final Project Gabungan v3\walkguide-mobile\walkguide_app"; D:\Tools\Flutter\flutter\bin\flutter.bat clean; Remove-Item -Recurse -Force .dart_tool, build -ErrorAction SilentlyContinue; D:\Tools\Flutter\flutter\bin\flutter.bat pub get
+
diff --git a/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml b/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml
index dfe830c..3ac360a 100644
--- a/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml
+++ b/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
-
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
-
-
-
-
+
+
-
-
-
\ No newline at end of file
diff --git a/walkguide-mobile/walkguide_app/assets/models/labels.txt b/walkguide-mobile/walkguide_app/assets/models/labels.txt
new file mode 100644
index 0000000..a7ffc28
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/assets/models/labels.txt
@@ -0,0 +1,10 @@
+person
+car
+motorcycle
+bicycle
+bus
+truck
+chair
+bench
+door
+stairs
diff --git a/walkguide-mobile/walkguide_app/lib/app/app.dart b/walkguide-mobile/walkguide_app/lib/app/app.dart
index e69de29..6e324e3 100644
--- a/walkguide-mobile/walkguide_app/lib/app/app.dart
+++ b/walkguide-mobile/walkguide_app/lib/app/app.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+import 'app_cubit.dart';
+import 'router.dart';
+
+class WalkGuideApp extends StatelessWidget {
+ const WalkGuideApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ const seed = Color(0xFF1A56DB);
+
+ return BlocProvider(
+ create: (_) => AppCubit(),
+ child: MaterialApp.router(
+ title: 'WalkGuide',
+ debugShowCheckedModeBanner: false,
+ routerConfig: appRouter,
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(seedColor: seed),
+ scaffoldBackgroundColor: const Color(0xFFF8FAFC),
+ textTheme: GoogleFonts.interTextTheme(),
+ appBarTheme: const AppBarTheme(
+ centerTitle: false,
+ backgroundColor: Colors.white,
+ foregroundColor: Color(0xFF0F172A),
+ elevation: 0,
+ surfaceTintColor: Colors.white,
+ ),
+ filledButtonTheme: FilledButtonThemeData(
+ style: FilledButton.styleFrom(
+ backgroundColor: seed,
+ foregroundColor: Colors.white,
+ minimumSize: const Size(0, 46),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ filled: true,
+ fillColor: Colors.white,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart b/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart
new file mode 100644
index 0000000..c775192
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart
@@ -0,0 +1,29 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class AppState {
+ final bool online;
+ final String? role;
+ final String? serverUrl;
+
+ const AppState({required this.online, this.role, this.serverUrl});
+
+ AppState copyWith({bool? online, String? role, String? serverUrl}) {
+ return AppState(
+ online: online ?? this.online,
+ role: role ?? this.role,
+ serverUrl: serverUrl ?? this.serverUrl,
+ );
+ }
+}
+
+class AppCubit extends Cubit {
+ AppCubit() : super(const AppState(online: true));
+
+ void setSession({required String role, required String serverUrl}) {
+ emit(state.copyWith(role: role, serverUrl: serverUrl, online: true));
+ }
+
+ void setOnline(bool value) => emit(state.copyWith(online: value));
+
+ void clearSession() => emit(const AppState(online: true));
+}
diff --git a/walkguide-mobile/walkguide_app/lib/app/injection_container.dart b/walkguide-mobile/walkguide_app/lib/app/injection_container.dart
index c14477f..0eb2cfa 100644
--- a/walkguide-mobile/walkguide_app/lib/app/injection_container.dart
+++ b/walkguide-mobile/walkguide_app/lib/app/injection_container.dart
@@ -1,38 +1,60 @@
import 'package:get_it/get_it.dart';
-import 'package:shared_preferences/shared_preferences.dart';
+import 'package:flutter/foundation.dart';
-import 'core/constants/app_constants.dart';
-import 'core/network/api_client.dart';
-import 'core/storage/secure_storage.dart';
-import 'core/services/tts_service.dart';
-import 'core/services/stt_service.dart';
-import 'core/services/voice_command_handler.dart';
-import 'core/services/haptic_service.dart';
+import '../core/constants/app_constants.dart';
+import '../core/ai/obstacle_analyzer.dart';
+import '../core/ai/yolo_detector.dart';
+import '../core/network/api_client.dart';
+import '../core/services/haptic_service.dart';
+import '../core/services/call_service.dart';
+import '../core/services/fcm_service.dart';
+import '../core/services/location_reporter_service.dart';
+import '../core/services/offline_queue_service.dart';
+import '../core/services/stt_service.dart';
+import '../core/services/tts_service.dart';
+import '../core/services/voice_command_handler.dart';
+import '../core/services/websocket_service.dart';
+import '../core/storage/secure_storage.dart';
final sl = GetIt.instance;
Future initDependencies() async {
- // ── Core singletons ──────────────────────────────────────────────────────
sl.registerLazySingleton(() => SecureStorage());
sl.registerLazySingleton(() => ApiClient(sl()));
-
sl.registerLazySingleton(() => TtsService());
sl.registerLazySingleton(() => SttService());
sl.registerLazySingleton(() => HapticService());
+ sl.registerLazySingleton(() => ObstacleAnalyzer());
+ sl.registerLazySingleton(() => YoloDetector(sl()));
+ sl.registerLazySingleton(() => OfflineQueueService());
+ sl.registerLazySingleton(() => FcmService(sl()));
+ sl.registerLazySingleton(() => WebSocketService(sl()));
+ sl.registerLazySingleton(() => LocationReporterService(sl(), sl()));
+ sl.registerLazySingleton(() => CallService(sl()));
sl.registerLazySingleton(
() => VoiceCommandHandler(sl(), sl()),
);
- // ── Init ApiClient if serverUrl already saved ─────────────────────────────
final serverUrl = await AppConstants.getServerUrl();
if (serverUrl != null && serverUrl.isNotEmpty) {
await sl().init(serverUrl);
}
- // ── Init TTS ──────────────────────────────────────────────────────────────
- await sl().init();
-
- // ── Init STT ──────────────────────────────────────────────────────────────
- await sl().init();
+ try {
+ await sl().init();
+ } catch (e) {
+ debugPrint('TTS init skipped: $e');
+ }
+ await sl().init();
+ if (!kIsWeb) {
+ try {
+ await sl().init();
+ } catch (e) {
+ debugPrint('STT init skipped: $e');
+ }
+ }
sl().loadDefaultCommands();
-}
\ No newline at end of file
+ if (!kIsWeb) {
+ await sl().init();
+ }
+}
diff --git a/walkguide-mobile/walkguide_app/lib/app/router.dart b/walkguide-mobile/walkguide_app/lib/app/router.dart
index cbc1aeb..028769a 100644
--- a/walkguide-mobile/walkguide_app/lib/app/router.dart
+++ b/walkguide-mobile/walkguide_app/lib/app/router.dart
@@ -1,110 +1,99 @@
-import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
-import 'core/storage/secure_storage.dart';
-import 'core/constants/app_constants.dart';
-import 'injection_container.dart';
-
-// Auth
-import 'features/server_connect/presentation/screens/server_connect_screen.dart';
-import 'features/auth/presentation/screens/splash_screen.dart';
-import 'features/auth/presentation/screens/login_screen.dart';
-import 'features/auth/presentation/screens/register_screen.dart';
-
-// User shell + screens
-import 'shared/widgets/user_shell.dart';
-import 'features/walk_guide/presentation/screens/walk_guide_screen.dart';
-import 'features/sos/presentation/screens/sos_screen.dart';
-import 'features/activity_log/presentation/screens/activity_log_screen.dart';
-import 'features/notifications/presentation/screens/notification_screen.dart';
-import 'features/navigation_mode/presentation/screens/navigation_mode_screen.dart';
-import 'features/settings/presentation/screens/user_settings_screen.dart';
-
-// Guardian shell + screens
-import 'shared/widgets/guardian_shell.dart';
-import 'features/guardian/presentation/screens/guardian_dashboard_screen.dart';
-import 'features/guardian/presentation/screens/guardian_map_screen.dart';
-import 'features/guardian/presentation/screens/guardian_activity_log_screen.dart';
-import 'features/guardian/presentation/screens/guardian_send_notif_screen.dart';
-import 'features/guardian/presentation/screens/guardian_ai_config_screen.dart';
-import 'features/guardian/presentation/screens/guardian_voice_cmd_screen.dart';
-import 'features/guardian/presentation/screens/guardian_shortcut_screen.dart';
-import 'features/guardian/presentation/screens/guardian_geofence_screen.dart';
-import 'features/pairing/presentation/screens/guardian_pairing_screen.dart';
-import 'features/pairing/presentation/screens/user_pairing_screen.dart';
-
-// Call
-import 'features/call/presentation/screens/call_screen.dart';
-import 'features/call/presentation/screens/incoming_call_screen.dart';
+import '../core/constants/app_constants.dart';
+import '../features/screens.dart';
+import '../shared/widgets/app_shells.dart';
final GoRouter appRouter = GoRouter(
initialLocation: '/splash',
redirect: (context, state) async {
- final serverUrl = await AppConstants.getServerUrl();
final path = state.matchedLocation;
+ final serverUrl = await AppConstants.getServerUrl();
- // 1. No server URL → must go to server-connect
- if (serverUrl == null || serverUrl.isEmpty) {
- if (path != '/server-connect') return '/server-connect';
- return null;
+ if ((serverUrl == null || serverUrl.isEmpty) && path != '/server-connect') {
+ return '/server-connect';
+ }
+ if (path == '/server-connect' &&
+ serverUrl != null &&
+ serverUrl.isNotEmpty) {
+ return '/splash';
}
-
- // 2. Already on server-connect but has URL → splash
- if (path == '/server-connect') return '/splash';
-
return null;
},
routes: [
GoRoute(
- path: '/server-connect',
- builder: (context, state) => const ServerConnectScreen(),
- ),
+ path: '/server-connect',
+ builder: (_, __) => const ServerConnectScreen()),
+ GoRoute(path: '/splash', builder: (_, __) => const SplashScreen()),
+ GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
+ GoRoute(path: '/register', builder: (_, __) => const RegisterScreen()),
GoRoute(
- path: '/splash',
- builder: (context, state) => const SplashScreen(),
- ),
- GoRoute(
- path: '/login',
- builder: (context, state) => const LoginScreen(),
- ),
- GoRoute(
- path: '/register',
- builder: (context, state) => const RegisterScreen(),
- ),
- GoRoute(
- path: '/incoming-call',
- builder: (context, state) => const IncomingCallScreen(),
- ),
-
- // ── USER SHELL ──────────────────────────────────────────────────────────
+ path: '/incoming-call', builder: (_, __) => const IncomingCallScreen()),
ShellRoute(
- builder: (context, state, child) => UserShell(child: child),
+ builder: (_, __, child) => UserShell(child: child),
routes: [
- GoRoute(path: '/user/walkguide', builder: (c, s) => const WalkGuideScreen()),
- GoRoute(path: '/user/sos', builder: (c, s) => const SosScreen()),
- GoRoute(path: '/user/activity', builder: (c, s) => const ActivityLogScreen()),
- GoRoute(path: '/user/notifications',builder: (c, s) => const NotificationScreen()),
- GoRoute(path: '/user/navigation', builder: (c, s) => const NavigationModeScreen()),
- GoRoute(path: '/user/settings', builder: (c, s) => const UserSettingsScreen()),
- GoRoute(path: '/user/pairing', builder: (c, s) => const UserPairingScreen()),
- GoRoute(path: '/user/call', builder: (c, s) => const CallScreen()),
+ GoRoute(
+ path: '/user/walkguide',
+ builder: (_, __) => const WalkGuideScreen()),
+ GoRoute(path: '/user/sos', builder: (_, __) => const SosScreen()),
+ GoRoute(
+ path: '/user/activity',
+ builder: (_, __) => const ActivityLogScreen()),
+ GoRoute(
+ path: '/user/notifications',
+ builder: (_, __) => const NotificationScreen()),
+ GoRoute(
+ path: '/user/navigation',
+ builder: (_, __) => const NavigationModeScreen()),
+ GoRoute(
+ path: '/user/settings',
+ builder: (_, __) => const UserSettingsScreen()),
+ GoRoute(
+ path: '/user/pairing',
+ builder: (_, __) => const UserPairingScreen()),
+ GoRoute(path: '/user/call', builder: (_, __) => const CallScreen()),
+ GoRoute(
+ path: '/user/benchmark',
+ builder: (_, __) => const AiBenchmarkScreen()),
],
),
-
- // ── GUARDIAN SHELL ──────────────────────────────────────────────────────
ShellRoute(
- builder: (context, state, child) => GuardianShell(child: child),
+ builder: (_, __, child) => GuardianShell(child: child),
routes: [
- GoRoute(path: '/guardian/dashboard', builder: (c, s) => const GuardianDashboardScreen()),
- GoRoute(path: '/guardian/map', builder: (c, s) => const GuardianMapScreen()),
- GoRoute(path: '/guardian/logs', builder: (c, s) => const GuardianActivityLogScreen()),
- GoRoute(path: '/guardian/send-notif', builder: (c, s) => const GuardianSendNotifScreen()),
- GoRoute(path: '/guardian/ai-config', builder: (c, s) => const GuardianAiConfigScreen()),
- GoRoute(path: '/guardian/voice-cmd', builder: (c, s) => const GuardianVoiceCmdScreen()),
- GoRoute(path: '/guardian/shortcuts', builder: (c, s) => const GuardianShortcutScreen()),
- GoRoute(path: '/guardian/geofence', builder: (c, s) => const GuardianGeofenceScreen()),
- GoRoute(path: '/guardian/pairing', builder: (c, s) => const GuardianPairingScreen()),
+ GoRoute(
+ path: '/guardian/dashboard',
+ builder: (_, __) => const GuardianDashboardScreen()),
+ GoRoute(
+ path: '/guardian/map',
+ builder: (_, __) => const GuardianMapScreen()),
+ GoRoute(
+ path: '/guardian/logs',
+ builder: (_, __) => const GuardianActivityLogScreen()),
+ GoRoute(
+ path: '/guardian/send-notif',
+ builder: (_, __) => const GuardianSendNotifScreen()),
+ GoRoute(
+ path: '/guardian/ai-config',
+ builder: (_, __) => const GuardianAiConfigScreen()),
+ GoRoute(
+ path: '/guardian/voice-cmd',
+ builder: (_, __) => const GuardianVoiceCmdScreen()),
+ GoRoute(
+ path: '/guardian/shortcuts',
+ builder: (_, __) => const GuardianShortcutScreen()),
+ GoRoute(
+ path: '/guardian/geofence',
+ builder: (_, __) => const GuardianGeofenceScreen()),
+ GoRoute(
+ path: '/guardian/pairing',
+ builder: (_, __) => const GuardianPairingScreen()),
+ GoRoute(
+ path: '/guardian/settings',
+ builder: (_, __) => const GuardianSettingsScreen()),
+ GoRoute(
+ path: '/guardian/benchmark',
+ builder: (_, __) => const AiBenchmarkScreen()),
],
),
],
-);
\ No newline at end of file
+);
diff --git a/walkguide-mobile/walkguide_app/lib/core/ai/detection_export.dart b/walkguide-mobile/walkguide_app/lib/core/ai/detection_export.dart
new file mode 100644
index 0000000..cf46cf5
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/lib/core/ai/detection_export.dart
@@ -0,0 +1,2 @@
+export 'obstacle_analyzer.dart';
+export 'yolo_detector.dart';
diff --git a/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart b/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart
new file mode 100644
index 0000000..8d06ca9
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/lib/core/ai/obstacle_analyzer.dart
@@ -0,0 +1,37 @@
+enum ObstacleDirection { left, center, right }
+
+class DetectionResult {
+ final String label;
+ final double confidence;
+ final ObstacleDirection direction;
+ final String estimatedDistance;
+
+ const DetectionResult({
+ required this.label,
+ required this.confidence,
+ required this.direction,
+ required this.estimatedDistance,
+ });
+
+ String get directionName => direction.name.toUpperCase();
+
+ String get spokenId {
+ final area = switch (direction) {
+ ObstacleDirection.left => 'kiri',
+ ObstacleDirection.center => 'tengah',
+ ObstacleDirection.right => 'kanan',
+ };
+ return 'Hati-hati, $label di $area. Jarak $estimatedDistance.';
+ }
+}
+
+class ObstacleAnalyzer {
+ DetectionResult analyzeFallback({String label = 'person', double confidence = 0.86}) {
+ return DetectionResult(
+ label: label,
+ confidence: confidence,
+ direction: ObstacleDirection.center,
+ estimatedDistance: 'Close',
+ );
+ }
+}
diff --git a/walkguide-mobile/walkguide_app/lib/core/ai/yolo_detector.dart b/walkguide-mobile/walkguide_app/lib/core/ai/yolo_detector.dart
new file mode 100644
index 0000000..098b0c4
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/lib/core/ai/yolo_detector.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import '../constants/app_constants.dart';
+import 'obstacle_analyzer.dart';
+
+class YoloDetector {
+ final ObstacleAnalyzer _analyzer;
+ List _labels = const [];
+ bool _ready = false;
+
+ YoloDetector(this._analyzer);
+
+ bool get isReady => _ready;
+
+ Future init() async {
+ try {
+ _labels = (await rootBundle.loadString(AppConstants.yoloLabelsPath))
+ .split('\n')
+ .map((e) => e.trim())
+ .where((e) => e.isNotEmpty)
+ .toList();
+ if (kIsWeb) {
+ _ready = false;
+ return;
+ }
+ await rootBundle.load(await AppConstants.getSelectedYoloModelPath());
+ _ready = true;
+ } catch (e) {
+ debugPrint('YOLO fallback mode: $e');
+ _ready = false;
+ }
+ }
+
+ Future detectFallback() async {
+ if (_ready) {
+ // Full tensor pre/post-processing belongs here once yolov8n.tflite is present.
+ // The app keeps a deterministic fallback so demo flows remain testable.
+ }
+ final label = _labels.isNotEmpty ? _labels.first : 'person';
+ return _analyzer.analyzeFallback(label: label);
+ }
+
+ void dispose() {
+ _ready = false;
+ }
+}
diff --git a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart
index 00aa885..e16a5df 100644
--- a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart
+++ b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart
@@ -2,6 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class AppConstants {
static const String _serverUrlKey = 'server_base_url';
+ static const String _selectedYoloModelKey = 'selected_yolo_model';
// Ambil base URL dari SharedPreferences
static Future getServerUrl() async {
@@ -12,18 +13,29 @@ class AppConstants {
// Simpan URL setelah berhasil connect
static Future setServerUrl(String url) async {
final prefs = await SharedPreferences.getInstance();
- // Trim trailing slash
- final cleaned = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
+ final cleaned = normalizeServerUrl(url);
await prefs.setString(_serverUrlKey, cleaned);
}
+ static String normalizeServerUrl(String url) {
+ var cleaned = url.trim();
+ if (!cleaned.startsWith('http://') && !cleaned.startsWith('https://')) {
+ cleaned = 'http://$cleaned';
+ }
+ while (cleaned.endsWith('/')) {
+ cleaned = cleaned.substring(0, cleaned.length - 1);
+ }
+ return cleaned;
+ }
+
static Future clearServerUrl() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_serverUrlKey);
}
static String buildApiUrl(String baseUrl) => '$baseUrl/api/v1';
- static String buildWsUrl(String baseUrl) => baseUrl.replaceFirst('http', 'ws') + '/ws';
+ static String buildWsUrl(String baseUrl) =>
+ '${baseUrl.replaceFirst('http', 'ws')}/ws';
// Timeouts
static const Duration connectTimeout = Duration(seconds: 10);
@@ -39,6 +51,16 @@ class AppConstants {
static const String yoloLabelsPath = 'assets/models/labels.txt';
static const int yoloInputSize = 640;
+ static Future getSelectedYoloModelPath() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getString(_selectedYoloModelKey) ?? yoloModelPath;
+ }
+
+ static Future setSelectedYoloModelPath(String path) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString(_selectedYoloModelKey, path);
+ }
+
// Agora - ganti dengan App ID dari agora.io
static const String agoraAppId = 'YOUR_AGORA_APP_ID';
-}
\ No newline at end of file
+}
diff --git a/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart b/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart
index c94717e..ec617ba 100644
--- a/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart
+++ b/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart
@@ -14,6 +14,7 @@ class ApiClient {
_dio = Dio(BaseOptions(
baseUrl: _baseUrl!,
connectTimeout: AppConstants.connectTimeout,
+ sendTimeout: AppConstants.connectTimeout,
receiveTimeout: AppConstants.receiveTimeout,
headers: {
'Content-Type': 'application/json',
@@ -97,4 +98,4 @@ class _ErrorInterceptor extends Interceptor {
// Let the calling code handle it — just pass through
handler.next(err);
}
-}
\ No newline at end of file
+}
diff --git a/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart
new file mode 100644
index 0000000..3926e82
--- /dev/null
+++ b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart
@@ -0,0 +1,54 @@
+import 'package:agora_rtc_engine/agora_rtc_engine.dart';
+import 'package:flutter/foundation.dart';
+
+import '../constants/app_constants.dart';
+import '../network/api_client.dart';
+
+class CallService {
+ final ApiClient _apiClient;
+ RtcEngine? _engine;
+
+ CallService(this._apiClient);
+
+ Future