import 'dart:async'; import 'dart:typed_data'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/crypto/key_manager.dart'; import 'audio_service.dart'; import 'secure_channel.dart'; /// Session lifecycle states. enum SessionState { noKey, // no passphrase / key loaded keyLoaded, // key ready, not yet in call inCall, // channel open, waiting for PTT or RX transmitting, receiving, error, } /// Top-level session manager exposed to the UI. /// /// Handles: /// • Passphrase entry and PBKDF2 key derivation (persists key hash only). /// • Creating / tearing down [SecureChannel]. /// • Delegating PTT and RX control. /// • Exposing a unified [onStateChange] stream and stats for the UI. class CallSession { final AudioService _audio; final KeyManager _keys = KeyManager(); SecureChannel? _channel; SessionState _state = SessionState.noKey; SessionState get state => _state; bool get hasKey => _keys.hasKey; String? _lastError; String? get lastError => _lastError; // Stream of [SessionState] for reactive UI updates. final _ctrl = StreamController.broadcast(); Stream get onStateChange => _ctrl.stream; CallSession(this._audio); // ── Key management ───────────────────────────────────────────────── /// Derive session key from [passphrase]. /// Optionally accepts a peer-supplied [salt] (from handshake packet). Future loadPassphrase(String passphrase, {Uint8List? peerSalt}) async { try { _keys.deriveKey(passphrase, salt: peerSalt); _setState(SessionState.keyLoaded); // Persist a salted SHA-256 fingerprint so the UI can show "key loaded" // without storing the raw passphrase. await _saveKeyFingerprint(passphrase); return true; } catch (e) { _lastError = 'Key derivation failed: $e'; _setState(SessionState.error); return false; } } /// Clear the current key (end session). Future clearKey() async { _keys.clear(); final prefs = await SharedPreferences.getInstance(); await prefs.remove('key_fingerprint'); _setState(SessionState.noKey); } /// Returns true if a fingerprint was previously saved (key loaded). Future hasSavedKey() async { final prefs = await SharedPreferences.getInstance(); return prefs.containsKey('key_fingerprint'); } // ── Call lifecycle ───────────────────────────────────────────────── /// Open the secure channel (call start). Future startCall() async { if (!_keys.hasKey) { _lastError = 'No key loaded'; _setState(SessionState.error); return; } _channel = SecureChannel(_audio, _keys); _channel!.onStateChange.listen(_onChannelState); await _channel!.open(); _setState(SessionState.inCall); } /// Close the secure channel (call end). Future endCall() async { await _channel?.close(); await _channel?.dispose(); _channel = null; if (_keys.hasKey) { _setState(SessionState.keyLoaded); } else { _setState(SessionState.noKey); } } // ── PTT ──────────────────────────────────────────────────────────── /// Press-to-talk: start encoding and transmitting voice. Future pttPress() async { await _channel?.startTransmit(); } /// Release PTT: stop transmitting. Future pttRelease() async { await _channel?.stopTransmit(); } // ── RX toggle ───────────────────────────────────────────────────── Future startListening() async { await _channel?.startReceive(); } Future stopListening() async { await _channel?.stopReceive(); } // ── Stats ────────────────────────────────────────────────────────── int get txFrames => _channel?.txFrames ?? 0; int get rxFrames => _channel?.rxFrames ?? 0; int get rxErrors => _channel?.rxErrors ?? 0; double get txRms => _channel?.txRms ?? 0.0; double get rxSignalRms => _channel?.rxSignalRms ?? 0.0; // ── Private helpers ──────────────────────────────────────────────── void _onChannelState(ChannelState cs) { switch (cs) { case ChannelState.transmitting: _setState(SessionState.transmitting); case ChannelState.receiving: _setState(SessionState.receiving); case ChannelState.txReady: _setState(SessionState.inCall); case ChannelState.error: _setState(SessionState.error); default: break; } } void _setState(SessionState s) { _state = s; _ctrl.add(s); } Future _saveKeyFingerprint(String passphrase) async { // Store a simple truncated hash for display — NOT the key itself. final hash = passphrase.codeUnits.fold(0, (prev, c) => (prev * 31 + c) & 0xFFFFFFFF); final prefs = await SharedPreferences.getInstance(); await prefs.setString('key_fingerprint', hash.toRadixString(16).padLeft(8, '0').toUpperCase()); } Future getSavedFingerprint() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('key_fingerprint'); } Future dispose() async { await endCall(); await _ctrl.close(); } }