answer method

Future<void> answer([
  1. String? sessionId
])

Answer the current inbound call — join as a WebRTC leg.

Implementation

Future<void> answer([String? sessionId]) async {
  var call = current.value;
  // The other side ended it just as we answered — don't join a dead session
  // (that hangs on "connecting"); make sure the native UI is gone and bail.
  final targetSid = sessionId ?? call?.sessionId;
  if (targetSid != null && _recentlyEnded.contains(targetSid)) {
    NexusLog.info('voice: answer ignored — $targetSid already ended by remote');
    _dismissNativeCall(targetSid);
    return;
  }
  // Cold-launch accept: the app was killed and the user accepted from the
  // native UI — reconstruct the inbound call from the session id in the event.
  if (call == null && sessionId != null) {
    call = NexusCall(
      sessionId: sessionId,
      direction: VoiceCallDirection.inbound,
      state: VoiceCallState.connecting,
      startedAt: DateTime.now(),
    );
    _set(call);
  }
  if (call == null || call.direction != VoiceCallDirection.inbound) return;
  // The accept can reach us via BOTH the CallKit event stream and the
  // cold-launch poll — answer each call exactly once, or we create two legs.
  if (_answeringSessionId == call.sessionId) return;
  _answeringSessionId = call.sessionId;
  NexusLog.info('voice: answering call ${call.sessionId}');
  // Do NOT prompt here: answering happens from the system call UI, often in a
  // background launch where the dialog cannot appear and the answer is an
  // instant meaningless refusal.
  if (!await _ensureMicPermission(allowPrompt: false)) {
    if (!kIsWeb && Platform.isIOS) {
      // iOS: the user just accepted this call on the CallKit screen. Failing it
      // over a permission we were never able to ask for would drop a live call,
      // so join anyway — LiveKit opens the mic the moment access is granted,
      // and the call is audible in the meantime in the other direction.
      NexusLog.error('voice: answering WITHOUT confirmed microphone access — the caller '
          'may not hear this device. Call Nexus.instance.voice.ensurePermissions() '
          'during onboarding so the prompt happens while the app is open.');
    } else {
      _fail(call.sessionId, 'microphone_denied');
      return;
    }
  }
  try {
    _set(call.copyWith(state: VoiceCallState.connecting));
    final myLeg = await _post('/partner/voice/legs', {
      'sessionId': call.sessionId,
      'role': 'callee',
      'endpointType': 'webrtc',
      'direction': 'inbound',
      'identityId': _identityId, // tag the media leg with WHO answered
    });
    final legId = _id(myLeg?['leg']);
    final token = NexusJoinToken.tryParse(myLeg?['join'] as Map<String, dynamic>?);
    if (legId == null || token == null) {
      NexusLog.warn('voice: answer join_failed (leg=$legId token=${token != null}) — ${myLeg?['error']}');
      _fail(call.sessionId, 'join_failed');
      return;
    }
    _set(current.value!.copyWith(legId: legId));
    NexusLog.info('voice: answered — joining media room "${token.room}" at ${token.url}');
    await _engine.connect(token);
    await _post('/partner/voice/legs/answer', {'legId': legId});
    final sid = call.sessionId;
    await _guard(() => _callKit.reportConnected(sid));
    _startPresence();
    // If the caller already hung up, no remote will ever join this room — don't
    // sit on "connecting" forever. End it if nobody appears shortly.
    _armConnectTimeout(sid);
  } catch (e) {
    NexusLog.error('voice.answer failed: $e');
    _fail(call.sessionId, 'error');
  }
}