working on background call handling
parent
84fe851ec8
commit
b8595cf796
@ -1,28 +1,104 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart';
|
||||
import 'package:test_sa/modules/cx_module/chat/call/services/webrtc_service.dart';
|
||||
import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart';
|
||||
import 'package:test_sa/modules/cx_module/chat/call/services/call_notification_service.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
/// Global service locator instance
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
/// Setup dependency injection
|
||||
/// This is called ONCE during app startup in the MAIN ISOLATE ONLY
|
||||
/// NEVER call this from background isolate
|
||||
Future<void> setupServiceLocator() async {
|
||||
// SignalR Service - ONLY ONE instance for the entire app
|
||||
getIt.registerLazySingleton<SignalRService>(
|
||||
() => SignalRService(),
|
||||
);
|
||||
|
||||
// WebRTC Service - ONLY ONE instance for the entire app
|
||||
getIt.registerLazySingleton<WebRTCService>(
|
||||
() => WebRTCService(),
|
||||
);
|
||||
|
||||
print('✅ [DI] Service locator initialized');
|
||||
print(' - SignalRService registered as singleton (hashCode: ${getIt<SignalRService>().hashCode})');
|
||||
print(' - WebRTCService registered as singleton (hashCode: ${getIt<WebRTCService>().hashCode})');
|
||||
log('🔧 [DI] Setting up service locator (main isolate)...', name: 'ServiceLocator');
|
||||
|
||||
// Register all services with duplicate checks
|
||||
await _registerServices();
|
||||
|
||||
log('✅ [DI] Service locator initialized', name: 'ServiceLocator');
|
||||
}
|
||||
|
||||
/// Internal helper to register services with duplicate protection
|
||||
Future<void> _registerServices() async {
|
||||
// 1. SignalR Service
|
||||
if (!getIt.isRegistered<SignalRService>()) {
|
||||
getIt.registerLazySingleton<SignalRService>(() => SignalRService());
|
||||
log(' ✅ SignalRService registered as singleton', name: 'ServiceLocator');
|
||||
} else {
|
||||
log(' ⚠️ SignalRService already registered, skipping', name: 'ServiceLocator');
|
||||
}
|
||||
|
||||
// 2. WebRTC Service
|
||||
if (!getIt.isRegistered<WebRTCService>()) {
|
||||
getIt.registerLazySingleton<WebRTCService>(() => WebRTCService());
|
||||
log(' ✅ WebRTCService registered as singleton', name: 'ServiceLocator');
|
||||
} else {
|
||||
log(' ⚠️ WebRTCService already registered, skipping', name: 'ServiceLocator');
|
||||
}
|
||||
|
||||
// 3. CallManager (depends on SignalRService, WebRTCService)
|
||||
if (!getIt.isRegistered<CallManager>()) {
|
||||
getIt.registerLazySingleton<CallManager>(() => CallManager());
|
||||
log(' ✅ CallManager registered as singleton', name: 'ServiceLocator');
|
||||
} else {
|
||||
log(' ⚠️ CallManager already registered, skipping', name: 'ServiceLocator');
|
||||
}
|
||||
|
||||
// 4. CallNotificationService (depends on CallManager)
|
||||
if (!getIt.isRegistered<CallNotificationService>()) {
|
||||
getIt.registerLazySingleton<CallNotificationService>(() => CallNotificationService());
|
||||
log(' ✅ CallNotificationService registered as singleton', name: 'ServiceLocator');
|
||||
} else {
|
||||
log(' ⚠️ CallNotificationService already registered, skipping', name: 'ServiceLocator');
|
||||
}
|
||||
|
||||
// Verify all critical dependencies are available
|
||||
_verifyDependencies();
|
||||
}
|
||||
|
||||
/// Verify that all required dependencies are registered
|
||||
void _verifyDependencies() {
|
||||
final dependencies = <Type>[
|
||||
SignalRService,
|
||||
WebRTCService,
|
||||
CallManager,
|
||||
CallNotificationService,
|
||||
];
|
||||
|
||||
log('🔍 [DI] Verifying dependencies...', name: 'ServiceLocator');
|
||||
bool allRegistered = true;
|
||||
|
||||
for (final type in dependencies) {
|
||||
if (getIt.isRegistered(type: type)) {
|
||||
log(' ✅ $type is registered', name: 'ServiceLocator');
|
||||
} else {
|
||||
log(' ❌ $type is NOT registered', name: 'ServiceLocator');
|
||||
allRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allRegistered) {
|
||||
log('✅ [DI] All dependencies verified successfully', name: 'ServiceLocator');
|
||||
} else {
|
||||
log('❌ [DI] Some dependencies are missing!', name: 'ServiceLocator');
|
||||
throw StateError('Required dependencies are not registered in GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all communication services
|
||||
/// Used for logout or testing
|
||||
Future<void> resetServices() async {
|
||||
await getIt<SignalRService>().reset();
|
||||
print('✅ [DI] All services reset');
|
||||
try {
|
||||
if (getIt.isRegistered<CallManager>()) {
|
||||
await getIt<CallManager>().reset();
|
||||
}
|
||||
if (getIt.isRegistered<SignalRService>()) {
|
||||
await getIt<SignalRService>().reset();
|
||||
}
|
||||
log('✅ [DI] All services reset', name: 'ServiceLocator');
|
||||
} catch (e) {
|
||||
log('❌ [DI] Error resetting services: $e', name: 'ServiceLocator');
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
import 'dart:developer';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Persistent storage for authentication credentials
|
||||
/// Used to restore SignalR connection in background/terminated app states
|
||||
class AuthStorage {
|
||||
// Storage keys
|
||||
static const String _keyAccessToken = 'signalr_access_token';
|
||||
static const String _keyUserId = 'signalr_user_id';
|
||||
static const String _keyEmployeeNumber = 'signalr_employee_number';
|
||||
|
||||
/// Save SignalR authentication credentials
|
||||
/// Called after successful login or token refresh
|
||||
static Future<bool> saveCredentials({
|
||||
required String accessToken,
|
||||
required String userId,
|
||||
String? employeeNumber,
|
||||
}) async {
|
||||
try {
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
log('💾 [AUTH_STORAGE] Saving SignalR credentials...', name: 'AuthStorage');
|
||||
log(' User ID: $userId', name: 'AuthStorage');
|
||||
log(' Employee Number: ${employeeNumber ?? "none"}', name: 'AuthStorage');
|
||||
log(' Token length: ${accessToken.length} chars', name: 'AuthStorage');
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final results = await Future.wait([
|
||||
prefs.setString(_keyAccessToken, accessToken),
|
||||
prefs.setString(_keyUserId, userId),
|
||||
if (employeeNumber != null)
|
||||
prefs.setString(_keyEmployeeNumber, employeeNumber),
|
||||
]);
|
||||
|
||||
final success = results.every((result) => result == true);
|
||||
|
||||
if (success) {
|
||||
log('✅ [AUTH_STORAGE] Credentials saved successfully', name: 'AuthStorage');
|
||||
} else {
|
||||
log('❌ [AUTH_STORAGE] Failed to save some credentials', name: 'AuthStorage');
|
||||
}
|
||||
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
return success;
|
||||
} catch (e, stackTrace) {
|
||||
log('❌ [AUTH_STORAGE] Error saving credentials: $e',
|
||||
name: 'AuthStorage', error: e, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stored access token
|
||||
static Future<String?> getAccessToken() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final token = prefs.getString(_keyAccessToken);
|
||||
|
||||
if (token != null) {
|
||||
log('✅ [AUTH_STORAGE] Access token retrieved (${token.length} chars)',
|
||||
name: 'AuthStorage');
|
||||
} else {
|
||||
log('⚠️ [AUTH_STORAGE] No access token found', name: 'AuthStorage');
|
||||
}
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
log('❌ [AUTH_STORAGE] Error reading access token: $e', name: 'AuthStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stored user ID
|
||||
static Future<String?> getUserId() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getString(_keyUserId);
|
||||
|
||||
if (userId != null) {
|
||||
log('✅ [AUTH_STORAGE] User ID retrieved: $userId', name: 'AuthStorage');
|
||||
} else {
|
||||
log('⚠️ [AUTH_STORAGE] No user ID found', name: 'AuthStorage');
|
||||
}
|
||||
|
||||
return userId;
|
||||
} catch (e) {
|
||||
log('❌ [AUTH_STORAGE] Error reading user ID: $e', name: 'AuthStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stored employee number
|
||||
static Future<String?> getEmployeeNumber() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final employeeNumber = prefs.getString(_keyEmployeeNumber);
|
||||
|
||||
if (employeeNumber != null) {
|
||||
log('✅ [AUTH_STORAGE] Employee number retrieved: $employeeNumber',
|
||||
name: 'AuthStorage');
|
||||
} else {
|
||||
log('⚠️ [AUTH_STORAGE] No employee number found', name: 'AuthStorage');
|
||||
}
|
||||
|
||||
return employeeNumber;
|
||||
} catch (e) {
|
||||
log('❌ [AUTH_STORAGE] Error reading employee number: $e', name: 'AuthStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all stored credentials at once
|
||||
/// Returns null if any required credential is missing
|
||||
static Future<StoredCredentials?> getCredentials() async {
|
||||
try {
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
log('🔍 [AUTH_STORAGE] Reading stored credentials...', name: 'AuthStorage');
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final accessToken = prefs.getString(_keyAccessToken);
|
||||
final userId = prefs.getString(_keyUserId);
|
||||
final employeeNumber = prefs.getString(_keyEmployeeNumber);
|
||||
|
||||
if (accessToken == null || userId == null) {
|
||||
log('❌ [AUTH_STORAGE] Missing required credentials', name: 'AuthStorage');
|
||||
log(' Access Token: ${accessToken != null ? "✓" : "✗"}', name: 'AuthStorage');
|
||||
log(' User ID: ${userId != null ? "✓" : "✗"}', name: 'AuthStorage');
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
return null;
|
||||
}
|
||||
|
||||
log('✅ [AUTH_STORAGE] All credentials retrieved successfully', name: 'AuthStorage');
|
||||
log(' User ID: $userId', name: 'AuthStorage');
|
||||
log(' Employee Number: ${employeeNumber ?? "none"}', name: 'AuthStorage');
|
||||
log(' Token length: ${accessToken.length} chars', name: 'AuthStorage');
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
|
||||
return StoredCredentials(
|
||||
accessToken: accessToken,
|
||||
userId: userId,
|
||||
employeeNumber: employeeNumber,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
log('❌ [AUTH_STORAGE] Error reading credentials: $e',
|
||||
name: 'AuthStorage', error: e, stackTrace: stackTrace);
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all stored authentication credentials
|
||||
/// Called during logout
|
||||
static Future<bool> clearCredentials() async {
|
||||
try {
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
log('🗑️ [AUTH_STORAGE] Clearing stored credentials...', name: 'AuthStorage');
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await Future.wait([
|
||||
prefs.remove(_keyAccessToken),
|
||||
prefs.remove(_keyUserId),
|
||||
prefs.remove(_keyEmployeeNumber),
|
||||
]);
|
||||
|
||||
log('✅ [AUTH_STORAGE] Credentials cleared successfully', name: 'AuthStorage');
|
||||
log('═══════════════════════════════════════════', name: 'AuthStorage');
|
||||
return true;
|
||||
} catch (e, stackTrace) {
|
||||
log('❌ [AUTH_STORAGE] Error clearing credentials: $e',
|
||||
name: 'AuthStorage', error: e, stackTrace: stackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if credentials are stored
|
||||
static Future<bool> hasStoredCredentials() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final hasToken = prefs.containsKey(_keyAccessToken);
|
||||
final hasUserId = prefs.containsKey(_keyUserId);
|
||||
return hasToken && hasUserId;
|
||||
} catch (e) {
|
||||
log('❌ [AUTH_STORAGE] Error checking credentials: $e', name: 'AuthStorage');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data class for stored credentials
|
||||
class StoredCredentials {
|
||||
final String accessToken;
|
||||
final String userId;
|
||||
final String? employeeNumber;
|
||||
|
||||
StoredCredentials({
|
||||
required this.accessToken,
|
||||
required this.userId,
|
||||
this.employeeNumber,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StoredCredentials(userId: $userId, employeeNumber: $employeeNumber, tokenLength: ${accessToken.length})';
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:test_sa/core/di/service_locator.dart';
|
||||
import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart';
|
||||
|
||||
/// App lifecycle observer to handle pending calls when app resumes
|
||||
class AppLifecycleObserver with WidgetsBindingObserver {
|
||||
static final AppLifecycleObserver _instance = AppLifecycleObserver._internal();
|
||||
factory AppLifecycleObserver() => _instance;
|
||||
AppLifecycleObserver._internal();
|
||||
|
||||
bool _isInitialized = false;
|
||||
bool _hasCheckedPendingCall = false;
|
||||
|
||||
/// Initialize the lifecycle observer
|
||||
void initialize() {
|
||||
if (_isInitialized) {
|
||||
log('⚠️ [LIFECYCLE] Already initialized', name: 'AppLifecycleObserver');
|
||||
return;
|
||||
}
|
||||
|
||||
log('🔧 [LIFECYCLE] Initializing app lifecycle observer', name: 'AppLifecycleObserver');
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_isInitialized = true;
|
||||
log('✅ [LIFECYCLE] Observer initialized', name: 'AppLifecycleObserver');
|
||||
|
||||
// Check for pending calls immediately on app start
|
||||
_checkPendingCallsOnStart();
|
||||
}
|
||||
|
||||
/// Dispose the observer
|
||||
void dispose() {
|
||||
if (_isInitialized) {
|
||||
log('🧹 [LIFECYCLE] Disposing lifecycle observer', name: 'AppLifecycleObserver');
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
log('═══════════════════════════════════════════', name: 'AppLifecycleObserver');
|
||||
log('🔄 [LIFECYCLE] App state changed: ${state.name}', name: 'AppLifecycleObserver');
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
log('✅ [LIFECYCLE] App resumed - checking for pending calls', name: 'AppLifecycleObserver');
|
||||
_onAppResumed();
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
log('⏸️ [LIFECYCLE] App inactive', name: 'AppLifecycleObserver');
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
log('⏸️ [LIFECYCLE] App paused', name: 'AppLifecycleObserver');
|
||||
break;
|
||||
case AppLifecycleState.detached:
|
||||
log('🔴 [LIFECYCLE] App detached', name: 'AppLifecycleObserver');
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
log('🔴 [LIFECYCLE] App hidden', name: 'AppLifecycleObserver');
|
||||
break;
|
||||
}
|
||||
log('═══════════════════════════════════════════', name: 'AppLifecycleObserver');
|
||||
}
|
||||
|
||||
/// Check for pending calls when app starts
|
||||
Future<void> _checkPendingCallsOnStart() async {
|
||||
try {
|
||||
// Wait a bit for app to fully initialize
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
log('═══════════════════════════════════════════', name: 'AppLifecycleObserver');
|
||||
log('🔍 [LIFECYCLE] Checking for pending calls on app start...', name: 'AppLifecycleObserver');
|
||||
|
||||
if (getIt.isRegistered<CallManager>()) {
|
||||
final callManager = getIt<CallManager>();
|
||||
await callManager.checkForPendingCalls();
|
||||
_hasCheckedPendingCall = true;
|
||||
log('✅ [LIFECYCLE] Pending call check complete', name: 'AppLifecycleObserver');
|
||||
} else {
|
||||
log('⚠️ [LIFECYCLE] CallManager not registered yet', name: 'AppLifecycleObserver');
|
||||
}
|
||||
|
||||
log('═══════════════════════════════════════════', name: 'AppLifecycleObserver');
|
||||
} catch (e, stackTrace) {
|
||||
log('❌ [LIFECYCLE] Error checking pending calls on start: $e\nStack: $stackTrace',
|
||||
name: 'AppLifecycleObserver');
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle app resumed state
|
||||
Future<void> _onAppResumed() async {
|
||||
try {
|
||||
// Check for pending calls if we haven't already
|
||||
if (!_hasCheckedPendingCall) {
|
||||
log('🔍 [LIFECYCLE] First resume - checking for pending calls', name: 'AppLifecycleObserver');
|
||||
if (getIt.isRegistered<CallManager>()) {
|
||||
final callManager = getIt<CallManager>();
|
||||
await callManager.checkForPendingCalls();
|
||||
_hasCheckedPendingCall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pending navigation (if call was accepted while app was launching)
|
||||
if (getIt.isRegistered<CallManager>()) {
|
||||
final callManager = getIt<CallManager>();
|
||||
await callManager.handlePendingNavigation();
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
log('❌ [LIFECYCLE] Error handling app resumed: $e\nStack: $stackTrace',
|
||||
name: 'AppLifecycleObserver');
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset state (for testing)
|
||||
void reset() {
|
||||
_hasCheckedPendingCall = false;
|
||||
log('🔄 [LIFECYCLE] State reset', name: 'AppLifecycleObserver');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue