213 lines
6.7 KiB
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;
|
|
}
|
|
}
|
|
}
|