MIF_E31222656/lib/services/session_manager.dart

656 lines
20 KiB
Dart

import 'dart:async';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/services/user_presence_service.dart';
class SessionManager {
static const String _lastActiveTimeKey = 'last_active_time';
static const String _lastUserInteractionKey = 'last_user_interaction';
static const String _sessionStateKey = 'session_state';
static const int _sessionTimeoutMinutes = 30;
static Timer? _sessionCheckTimer;
static Timer? _presenceUpdateTimer;
static bool _isCheckingSession = false;
static bool _isAppInBackground = false;
static bool _isSessionExpired = false;
static bool _hasLoggedInUser = false;
static bool _isAppJustLaunched = true;
static final StreamController<bool> _sessionExpiredController =
StreamController<bool>.broadcast();
// Stream untuk mendengarkan perubahan status session
static Stream<bool> get sessionExpiredStream =>
_sessionExpiredController.stream;
// Getter untuk mendapatkan waktu timeout session dalam menit
static int getSessionTimeout() {
return _sessionTimeoutMinutes;
}
// Getter untuk status login
static bool get hasLoggedInUser => _hasLoggedInUser;
// Metode untuk menghindari blocking pada SharedPreferences
static Future<SharedPreferences?> _getSafeSharedPreferences() async {
try {
return await SharedPreferences.getInstance().timeout(
const Duration(seconds: 2),
onTimeout: () {
debugPrint('Session: SharedPreferences timeout');
throw TimeoutException('SharedPreferences timeout');
},
);
} catch (e) {
debugPrint('Session: Error getting SharedPreferences - $e');
return null;
}
}
// Initialize session management
static Future<void> initializeSession() async {
try {
_isAppJustLaunched = true;
debugPrint('Session: App just launched flag set to true');
Future.delayed(Duration(seconds: 10), () {
_isAppJustLaunched = false;
debugPrint('Session: App just launched flag set to false after delay');
});
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint('Session: No authenticated user found');
_hasLoggedInUser = false;
return;
}
_hasLoggedInUser = true;
await updateLastUserInteraction();
_isAppInBackground = false;
_setSessionExpired(false);
// Initialize user presence service
try {
if (GetIt.instance.isRegistered<UserPresenceService>()) {
await GetIt.instance<UserPresenceService>().initialize();
_startPresenceUpdates();
}
} catch (e) {
debugPrint('Session: Error initializing presence service - $e');
}
Future.delayed(Duration(seconds: 5), () {
_startSessionMonitoring();
});
debugPrint(
'Session: Initialized successfully for user: ${currentUser.email}',
);
} catch (e) {
debugPrint('Session: Error initializing - $e');
_hasLoggedInUser = false;
}
}
// Method baru untuk memperbarui status login
static void setUserLoggedIn(bool isLoggedIn) {
_hasLoggedInUser = isLoggedIn;
debugPrint('Session: User login status set to: $isLoggedIn');
if (isLoggedIn) {
_setSessionExpired(false);
updateLastUserInteraction();
_startSessionMonitoring();
_startPresenceUpdates();
} else {
_stopSessionMonitoring();
_stopPresenceUpdates();
}
}
// Start periodic presence updates
static void _startPresenceUpdates() {
_stopPresenceUpdates(); // Stop any existing timer
// Update presence every 30 seconds
_presenceUpdateTimer = Timer.periodic(Duration(seconds: 30), (timer) {
if (_hasLoggedInUser && !_isSessionExpired) {
try {
if (GetIt.instance.isRegistered<UserPresenceService>()) {
GetIt.instance<UserPresenceService>().updatePresence();
}
} catch (e) {
debugPrint('Session: Error updating presence - $e');
}
}
});
debugPrint('Session: Started presence updates');
}
// Stop presence updates
static void _stopPresenceUpdates() {
_presenceUpdateTimer?.cancel();
_presenceUpdateTimer = null;
}
// Check if session is valid with improved logic
static Future<bool> isSessionValid() async {
debugPrint('Session: Checking session validity...');
// Jika aplikasi baru saja diluncurkan, asumsikan sesi valid untuk menghindari dialog terlalu dini
if (_isAppJustLaunched) {
debugPrint('Session: App just launched, assuming session valid');
return true;
}
// If session is already marked as expired, return false immediately
if (_isSessionExpired) {
debugPrint('Session: Session already marked as expired');
return false;
}
if (!_hasLoggedInUser) {
debugPrint('Session: No logged in user, skipping session validity check');
return true;
}
if (_isCheckingSession) {
debugPrint('Session: Already checking session, returning current status');
return !_isSessionExpired;
}
_isCheckingSession = true;
try {
User? currentUser;
Session? currentSession;
try {
currentUser = Supabase.instance.client.auth.currentUser;
currentSession = Supabase.instance.client.auth.currentSession;
debugPrint('Session: Supabase user: ${currentUser?.id ?? "null"}');
debugPrint(
'Session: Supabase session: ${currentSession != null ? "exists" : "null"}',
);
if (currentSession != null) {
final expiresAt = currentSession.expiresAt;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
debugPrint(
'Session: Token expires at: $expiresAt, current time: $now',
);
}
} catch (e) {
debugPrint('Session: Error accessing Supabase auth - $e');
_isCheckingSession = false;
return !_isSessionExpired;
}
// If no user or session, session is invalid
if (currentUser == null || currentSession == null) {
debugPrint('Session: No valid Supabase user or session found');
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
SharedPreferences? prefs = await _getSafeSharedPreferences();
final sessionState = prefs?.getString(_sessionStateKey);
if (sessionState == null || sessionState == 'inactive') {
debugPrint(
'Session: No session state found or inactive, marking as expired',
);
_hasLoggedInUser = false;
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
// Check if Supabase token is expired
final sessionExpiry = currentSession.expiresAt;
if (sessionExpiry != null &&
sessionExpiry <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
debugPrint('Session: Supabase session token expired');
_hasLoggedInUser = false;
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
if (prefs == null) {
debugPrint(
'Session: Could not access SharedPreferences, assuming valid',
);
_isCheckingSession = false;
return !_isSessionExpired;
}
final lastInteractionTime = prefs.getInt(_lastUserInteractionKey);
debugPrint(
'Session: Last user interaction time from prefs: $lastInteractionTime',
);
if (lastInteractionTime == null) {
debugPrint(
'Session: No user interaction timestamp found, setting current time',
);
await updateLastUserInteraction();
_isCheckingSession = false;
return !_isSessionExpired;
}
final lastInteraction = DateTime.fromMillisecondsSinceEpoch(
lastInteractionTime,
);
final now = DateTime.now();
debugPrint('Session: Last user interaction time: $lastInteraction');
debugPrint('Session: Current time: $now');
if (lastInteraction.isAfter(now)) {
debugPrint('Session: Invalid timestamp detected (future date)');
await updateLastUserInteraction();
_isCheckingSession = false;
return !_isSessionExpired;
}
final difference = now.difference(lastInteraction);
final differenceInMinutes = difference.inMinutes;
final differenceInSeconds = difference.inSeconds;
debugPrint(
'Session: Time difference: $differenceInMinutes minutes, $differenceInSeconds seconds',
);
debugPrint(
'Session: Timeout limit: $_sessionTimeoutMinutes minutes (${_sessionTimeoutMinutes * 60} seconds)',
);
final timeoutInSeconds = _sessionTimeoutMinutes * 60;
final isValid = differenceInSeconds < timeoutInSeconds;
if (!isValid) {
debugPrint(
'Session: TIMEOUT - Sesi kedaluwarsa setelah $differenceInMinutes menit ($differenceInSeconds detik) tidak aktif. Batas waktu: $_sessionTimeoutMinutes menit',
);
_setSessionExpired(true);
} else {
_setSessionExpired(false);
debugPrint(
'Session: VALID - Terakhir aktif $differenceInMinutes menit ($differenceInSeconds detik) yang lalu. Batas waktu: $_sessionTimeoutMinutes menit',
);
}
_isCheckingSession = false;
return isValid;
} catch (e) {
debugPrint('Session: Error checking validity - $e');
_isCheckingSession = false;
return !_isSessionExpired;
}
}
// BARU: Update timestamp interaksi pengguna terakhir
static Future<void> updateLastUserInteraction() async {
try {
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint(
'Session: Cannot update interaction - user not authenticated',
);
return;
}
SharedPreferences? prefs = await _getSafeSharedPreferences();
if (prefs == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
try {
await prefs
.setInt(_lastUserInteractionKey, now)
.timeout(
const Duration(seconds: 2),
onTimeout: () {
debugPrint(
'Session: Timeout setting last user interaction time',
);
return false;
},
);
await updateLastActiveTime();
// Update presence when user interacts
try {
if (GetIt.instance.isRegistered<UserPresenceService>()) {
GetIt.instance<UserPresenceService>().updatePresence();
}
} catch (e) {
debugPrint('Session: Error updating presence on interaction - $e');
}
debugPrint(
'Session: User interaction recorded at ${DateTime.fromMillisecondsSinceEpoch(now)}',
);
} catch (e) {
debugPrint(
'Session: Error writing user interaction to SharedPreferences - $e',
);
}
} catch (e) {
debugPrint('Session: Error updating user interaction - $e');
}
}
// Update last active time with better error handling
static Future<void> updateLastActiveTime() async {
try {
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint('Session: Cannot update activity - user not authenticated');
return;
}
SharedPreferences? prefs = await _getSafeSharedPreferences();
if (prefs == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
try {
await prefs
.setInt(_lastActiveTimeKey, now)
.timeout(
const Duration(seconds: 2),
onTimeout: () {
debugPrint('Session: Timeout setting last active time');
return false;
},
);
await prefs
.setString(_sessionStateKey, 'active')
.timeout(
const Duration(seconds: 2),
onTimeout: () {
debugPrint('Session: Timeout setting session state');
return false;
},
);
debugPrint(
'Session: Activity updated at ${DateTime.fromMillisecondsSinceEpoch(now)}',
);
} catch (e) {
debugPrint('Session: Error writing to SharedPreferences - $e');
}
} catch (e) {
debugPrint('Session: Error updating activity - $e');
}
}
// Called when app goes to background
static Future<void> onAppBackground() async {
debugPrint('Session: App entering background');
_isAppInBackground = true;
await updateLastActiveTime();
_startSessionMonitoring();
}
// Called when app comes to foreground
static Future<void> onAppForeground() async {
debugPrint('Session: App entering foreground');
if (!_isAppInBackground) return;
_isAppInBackground = false;
try {
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint(
'Session: No authenticated user when returning to foreground',
);
_stopSessionMonitoring();
return;
}
debugPrint(
'Session: Checking session validity after returning to foreground',
);
SharedPreferences? prefs = await _getSafeSharedPreferences();
if (prefs != null) {
final lastInteractionTime = prefs.getInt(_lastUserInteractionKey);
if (lastInteractionTime != null) {
final lastInteraction = DateTime.fromMillisecondsSinceEpoch(
lastInteractionTime,
);
final now = DateTime.now();
final difference = now.difference(lastInteraction);
final differenceInSeconds = difference.inSeconds;
final timeoutInSeconds = _sessionTimeoutMinutes * 60;
debugPrint('Session: Last user interaction time: $lastInteraction');
debugPrint('Session: Current time: $now');
debugPrint(
'Session: Time difference: ${difference.inMinutes} minutes, $differenceInSeconds seconds',
);
debugPrint(
'Session: Timeout limit: $_sessionTimeoutMinutes minutes ($timeoutInSeconds seconds)',
);
if (differenceInSeconds >= timeoutInSeconds) {
debugPrint(
'Session: TIMEOUT after foreground - Inactive for $differenceInSeconds seconds (limit: $timeoutInSeconds)',
);
_setSessionExpired(true);
await clearSession();
return;
}
}
}
final isValid = await isSessionValid();
if (!isValid) {
debugPrint('Session: Expired while in background');
await clearSession();
Future.delayed(const Duration(milliseconds: 500), () {
_setSessionExpired(true);
});
} else {
debugPrint('Session: Still valid after background');
await updateLastActiveTime();
_startSessionMonitoring();
}
} catch (e) {
debugPrint('Session: Error during foreground transition - $e');
}
}
// Set session expired state and notify listeners
static void _setSessionExpired(bool value) {
if (_isSessionExpired != value) {
_isSessionExpired = value;
debugPrint('Session: Setting expired state to $value');
_sessionExpiredController.add(value);
// If session is expired, force clear session
if (value) {
debugPrint('Session: Session expired, clearing session data');
clearSession();
}
}
}
// Get session status for UI components
static bool get isExpired => _isSessionExpired;
// Check if user is properly authenticated
static bool get isAuthenticated {
if (_isSessionExpired) {
debugPrint('Session: Session is marked as expired, not authenticated');
return false;
}
if (!_hasLoggedInUser) {
return false;
}
final currentUser = Supabase.instance.client.auth.currentUser;
final currentSession = Supabase.instance.client.auth.currentSession;
final isValid =
currentUser != null && currentSession != null && !_isSessionExpired;
if (!isValid && _hasLoggedInUser) {
_hasLoggedInUser = false;
_setSessionExpired(true);
debugPrint(
'Session: Updated login status to false based on authentication check',
);
}
return isValid;
}
// Clear session data with proper cleanup
static Future<void> clearSession() async {
try {
_stopSessionMonitoring();
_setSessionExpired(true);
_hasLoggedInUser = false;
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_lastActiveTimeKey);
await prefs.remove(_lastUserInteractionKey);
await prefs.remove(_sessionStateKey);
await Supabase.instance.client.auth.signOut();
debugPrint('Session: Cleared and signed out successfully');
} catch (e) {
debugPrint('Session: Error during cleanup - $e');
_setSessionExpired(true);
_hasLoggedInUser = false;
}
}
// Refresh session - extend if still valid
static Future<bool> refreshSession() async {
try {
final isValid = await isSessionValid();
if (isValid) {
await updateLastUserInteraction();
return true;
}
return false;
} catch (e) {
debugPrint('Session: Error refreshing - $e');
return false;
}
}
// Fungsi baru untuk memberi tahu aplikasi tentang aktivitas penting yang harus memperbarui sesi
static Future<void> notifyImportantActivity(String activityType) async {
try {
debugPrint('Session: Important activity detected: $activityType');
await updateLastUserInteraction();
Timer(Duration(seconds: 2), () {
debugPrint('Session: Follow-up check after important activity');
isSessionValid();
});
} catch (e) {
debugPrint('Session: Error handling important activity - $e');
}
}
// Start session monitoring (check every 30 seconds)
static void _startSessionMonitoring() {
_stopSessionMonitoring();
debugPrint(
'Session: Starting monitoring with timeout: $_sessionTimeoutMinutes minutes',
);
_sessionCheckTimer = Timer.periodic(const Duration(seconds: 15), (
timer,
) async {
debugPrint('Session: Running periodic check...');
if (_isCheckingSession) {
debugPrint('Session: Skipping periodic check (already checking)');
return;
}
_isCheckingSession = true;
try {
bool isValid = false;
try {
isValid = await isSessionValid().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('Session: Validity check timed out');
return true;
},
);
} catch (e) {
debugPrint('Session: Error during periodic check - $e');
isValid = true;
}
if (!isValid) {
debugPrint('Session: Expired during periodic check');
await clearSession();
_setSessionExpired(true);
_stopSessionMonitoring();
}
} catch (e) {
debugPrint('Session: Error during periodic check - $e');
} finally {
_isCheckingSession = false;
}
});
debugPrint('Session: Monitoring started');
Future.delayed(Duration(seconds: 5), () async {
debugPrint('Session: Immediate check after monitoring start');
await isSessionValid();
});
}
// Stop session monitoring
static void _stopSessionMonitoring() {
_sessionCheckTimer?.cancel();
_sessionCheckTimer = null;
_stopPresenceUpdates();
debugPrint('Session: Monitoring stopped');
}
// Dispose resources
static void dispose() {
_sessionCheckTimer?.cancel();
_presenceUpdateTimer?.cancel();
_sessionExpiredController.close();
try {
if (GetIt.instance.isRegistered<UserPresenceService>()) {
GetIt.instance<UserPresenceService>().dispose();
}
} catch (e) {
debugPrint('Session: Error disposing presence service - $e');
}
}
}