sojorn/sojorn_app/lib/services/chat_backup_manager.dart

213 lines
6.7 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'simple_e2ee_service.dart';
import 'local_key_backup_service.dart';
import 'local_message_store.dart';
import 'auth_service.dart';
/// Manages automatic encrypted cloud backups of chat data.
///
/// Flow: User enables backup → sets password once → chats auto-backup.
/// To restore on another device, user enters the same password.
class ChatBackupManager {
static final ChatBackupManager _instance = ChatBackupManager._internal();
static ChatBackupManager get instance => _instance;
ChatBackupManager._internal();
static const String _passwordKey = 'chat_backup_password_v1';
static const String _enabledKey = 'chat_backup_enabled';
static const String _lastBackupKey = 'chat_backup_last_at';
static const String _lastBackupCountKey = 'chat_backup_last_msg_count';
static const Duration _minBackupInterval = Duration(minutes: 10);
static const int _minNewMessages = 1;
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
final SimpleE2EEService _e2ee = SimpleE2EEService();
Timer? _debounceTimer;
bool _backupInProgress = false;
DateTime? _lastBackupAt;
int _lastBackupMessageCount = 0;
bool? _enabledCache;
/// Whether backup has been set up (password stored + enabled)
Future<bool> get isEnabled async {
if (_enabledCache != null) return _enabledCache!;
final prefs = await SharedPreferences.getInstance();
_enabledCache = prefs.getBool(_enabledKey) ?? false;
return _enabledCache!;
}
/// Whether a backup password has been configured
Future<bool> get hasPassword async {
final pw = await _secureStorage.read(key: _passwordKey);
return pw != null && pw.isNotEmpty;
}
/// Last backup timestamp
Future<DateTime?> get lastBackupAt async {
if (_lastBackupAt != null) return _lastBackupAt;
final prefs = await SharedPreferences.getInstance();
final ms = prefs.getInt(_lastBackupKey);
if (ms != null) {
_lastBackupAt = DateTime.fromMillisecondsSinceEpoch(ms);
}
return _lastBackupAt;
}
/// Number of messages in last backup
Future<int> get lastBackupMessageCount async {
if (_lastBackupMessageCount > 0) return _lastBackupMessageCount;
final prefs = await SharedPreferences.getInstance();
_lastBackupMessageCount = prefs.getInt(_lastBackupCountKey) ?? 0;
return _lastBackupMessageCount;
}
/// Set up backup: store password and enable auto-backup.
/// Called once during the setup flow.
Future<void> enable(String password) async {
await _secureStorage.write(key: _passwordKey, value: password);
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_enabledKey, true);
_enabledCache = true;
// Perform initial backup immediately
await _performBackup();
}
/// Disable auto-backup and clear stored password.
Future<void> disable() async {
await _secureStorage.delete(key: _passwordKey);
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_enabledKey, false);
_enabledCache = false;
_debounceTimer?.cancel();
}
/// Change the backup password. Re-encrypts and uploads immediately.
Future<void> changePassword(String newPassword) async {
await _secureStorage.write(key: _passwordKey, value: newPassword);
await _performBackup();
}
/// Schedule a backup after a debounce period.
/// Call this after sending/receiving messages.
void scheduleBackup() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(seconds: 30), () {
_tryAutoBackup();
});
}
/// Trigger an immediate backup if conditions are met.
/// Called by SyncManager after sync, or on app background.
Future<void> triggerBackupIfNeeded() async {
await _tryAutoBackup();
}
/// Force an immediate backup regardless of schedule.
Future<void> forceBackup() async {
await _performBackup();
}
/// Restore from cloud backup using the given password.
/// Returns restore result with counts.
Future<Map<String, dynamic>> restoreFromCloud(String password) async {
return await LocalKeyBackupService.restoreFromCloud(
password: password,
e2eeService: _e2ee,
);
}
/// Get current local message count for status display.
Future<int> getLocalMessageCount() async {
final records = await LocalMessageStore.instance.getAllMessageRecords();
return records.length;
}
/// Clear all local message data.
Future<void> clearLocalMessages() async {
await LocalMessageStore.instance.clearAll();
}
/// Reset everything on sign-out.
void reset() {
_debounceTimer?.cancel();
_lastBackupAt = null;
_lastBackupMessageCount = 0;
_enabledCache = null;
_backupInProgress = false;
}
// --- Private ---
Future<void> _tryAutoBackup() async {
if (_backupInProgress) return;
if (!AuthService.instance.isAuthenticated) return;
final enabled = await isEnabled;
if (!enabled) return;
final hasPw = await hasPassword;
if (!hasPw) return;
// Check minimum interval
final last = await lastBackupAt;
if (last != null && DateTime.now().difference(last) < _minBackupInterval) {
return;
}
// Check if there are new messages since last backup
final currentCount = await getLocalMessageCount();
final lastCount = await lastBackupMessageCount;
if (currentCount <= 0) return;
if (currentCount - lastCount < _minNewMessages && last != null) return;
await _performBackup();
}
Future<void> _performBackup() async {
if (_backupInProgress) return;
_backupInProgress = true;
try {
final password = await _secureStorage.read(key: _passwordKey);
if (password == null || password.isEmpty) return;
if (!_e2ee.isReady) {
await _e2ee.initialize();
if (!_e2ee.isReady) return;
}
final backup = await LocalKeyBackupService.createEncryptedBackup(
password: password,
e2eeService: _e2ee,
includeMessages: true,
includeKeys: true,
);
await LocalKeyBackupService.uploadToCloud(backup: backup);
// Record success
final now = DateTime.now();
_lastBackupAt = now;
final msgCount = await getLocalMessageCount();
_lastBackupMessageCount = msgCount;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_lastBackupKey, now.millisecondsSinceEpoch);
await prefs.setInt(_lastBackupCountKey, msgCount);
debugPrint('[ChatBackup] Auto-backup complete: $msgCount messages');
} catch (e) {
debugPrint('[ChatBackup] Auto-backup failed: $e');
} finally {
_backupInProgress = false;
}
}
}