|
|
|
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
|
|
|
|
|
|
import '../../app/injection_container.dart';
|
|
|
|
|
import '../../core/errors/friendly_error.dart';
|
|
|
|
|
import '../../core/services/call_service.dart';
|
|
|
|
|
import '../../core/services/haptic_service.dart';
|
|
|
|
|
import '../../core/services/tts_service.dart';
|
|
|
|
@@ -63,65 +64,71 @@ class _CallScreenState extends State<CallScreen>
|
|
|
|
|
unawaited(_finishRemoteEnded());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final invite = await callService.startPairedCall();
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
if (invite == null) {
|
|
|
|
|
final invite = await runFriendly<Map<String, dynamic>>(
|
|
|
|
|
() => callService.startPairedCall(),
|
|
|
|
|
onError: _failCall,
|
|
|
|
|
fallback: 'Panggilan gagal. Server tidak merespons.',
|
|
|
|
|
);
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
if (invite == null) {
|
|
|
|
|
if (_phase != _CallPhase.failed) {
|
|
|
|
|
_failCall('Panggilan gagal. Pastikan sudah pairing dan server aktif.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_otherId = _asInt(invite['receiverId']);
|
|
|
|
|
_activeChannel = invite['channelName']?.toString();
|
|
|
|
|
setState(() => _phase = _CallPhase.calling);
|
|
|
|
|
sl<TtsService>().speak(
|
|
|
|
|
'Panggilan dikirim ke ${widget.targetLabel}. Menunggu jawaban.',
|
|
|
|
|
);
|
|
|
|
|
_startAcceptedPolling();
|
|
|
|
|
_ringTimeout?.cancel();
|
|
|
|
|
_ringTimeout = Timer(const Duration(seconds: 45), () {
|
|
|
|
|
if (!mounted || _phase == _CallPhase.connected) return;
|
|
|
|
|
_failCall('Panggilan tidak dijawab.');
|
|
|
|
|
});
|
|
|
|
|
} catch (_) {
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
_failCall('Panggilan gagal. Server tidak merespons.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_otherId = _asInt(invite['receiverId']);
|
|
|
|
|
_activeChannel = invite['channelName']?.toString();
|
|
|
|
|
setState(() => _phase = _CallPhase.calling);
|
|
|
|
|
sl<TtsService>().speak(
|
|
|
|
|
'Panggilan dikirim ke ${widget.targetLabel}. Menunggu jawaban.',
|
|
|
|
|
);
|
|
|
|
|
_startAcceptedPolling();
|
|
|
|
|
_ringTimeout?.cancel();
|
|
|
|
|
_ringTimeout = Timer(const Duration(seconds: 45), () {
|
|
|
|
|
if (!mounted || _phase == _CallPhase.connected) return;
|
|
|
|
|
_failCall('Panggilan tidak dijawab.');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _startAcceptedPolling() {
|
|
|
|
|
_acceptedPoll?.cancel();
|
|
|
|
|
_acceptedPoll = Timer.periodic(const Duration(seconds: 2), (_) async {
|
|
|
|
|
if (!mounted || _activeChannel == null) return;
|
|
|
|
|
try {
|
|
|
|
|
final state = await sl<CallService>()
|
|
|
|
|
final state = await runFriendly<Map<String, dynamic>>(
|
|
|
|
|
() => sl<CallService>()
|
|
|
|
|
.getCallState(_activeChannel)
|
|
|
|
|
.timeout(const Duration(seconds: 3));
|
|
|
|
|
final status = state?['status']?.toString();
|
|
|
|
|
if (status == 'ENDED') {
|
|
|
|
|
await _finishRemoteEnded();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (status == 'ACCEPTED') {
|
|
|
|
|
_markRemoteConnected();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final accepted = await sl<CallService>()
|
|
|
|
|
.getAcceptedCall()
|
|
|
|
|
.timeout(const Duration(seconds: 3));
|
|
|
|
|
if (accepted?['type']?.toString() != 'CALL_ACCEPTED') return;
|
|
|
|
|
final channel = accepted?['channelName']?.toString();
|
|
|
|
|
if (_activeChannel != null &&
|
|
|
|
|
channel != null &&
|
|
|
|
|
channel.isNotEmpty &&
|
|
|
|
|
channel != _activeChannel) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_markRemoteConnected();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Keep ringing; a short network hiccup should not cancel the call UI.
|
|
|
|
|
.timeout(const Duration(seconds: 3)),
|
|
|
|
|
onError: (_) {},
|
|
|
|
|
fallback: 'Polling panggilan gagal.',
|
|
|
|
|
);
|
|
|
|
|
if (state == null) return;
|
|
|
|
|
final status = state['status']?.toString();
|
|
|
|
|
if (status == 'ENDED') {
|
|
|
|
|
await _finishRemoteEnded();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (status == 'ACCEPTED') {
|
|
|
|
|
_markRemoteConnected();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final accepted = await runFriendly<Map<String, dynamic>>(
|
|
|
|
|
() => sl<CallService>()
|
|
|
|
|
.getAcceptedCall()
|
|
|
|
|
.timeout(const Duration(seconds: 3)),
|
|
|
|
|
onError: (_) {},
|
|
|
|
|
fallback: 'Polling panggilan diterima gagal.',
|
|
|
|
|
);
|
|
|
|
|
if (accepted?['type']?.toString() != 'CALL_ACCEPTED') return;
|
|
|
|
|
final channel = accepted?['channelName']?.toString();
|
|
|
|
|
if (_activeChannel != null &&
|
|
|
|
|
channel != null &&
|
|
|
|
|
channel.isNotEmpty &&
|
|
|
|
|
channel != _activeChannel) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_markRemoteConnected();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -319,7 +326,12 @@ class _IncomingCallScreenState extends State<IncomingCallScreen> {
|
|
|
|
|
setState(() => _responding = true);
|
|
|
|
|
sl<TtsService>().speak('Menerima panggilan.');
|
|
|
|
|
|
|
|
|
|
final joined = await _joinIncomingChannel();
|
|
|
|
|
final joined = await runFriendly<bool>(
|
|
|
|
|
() => _joinIncomingChannel(),
|
|
|
|
|
onError: (_) {},
|
|
|
|
|
fallback: 'Panggilan gagal tersambung.',
|
|
|
|
|
) ??
|
|
|
|
|
false;
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
if (!joined || _joinedChannel == null || widget.callerId == null) {
|
|
|
|
|
setState(() {
|
|
|
|
@@ -350,14 +362,16 @@ class _IncomingCallScreenState extends State<IncomingCallScreen> {
|
|
|
|
|
_statePoll?.cancel();
|
|
|
|
|
_statePoll = Timer.periodic(const Duration(seconds: 2), (_) async {
|
|
|
|
|
if (!mounted || _joinedChannel == null) return;
|
|
|
|
|
try {
|
|
|
|
|
final state = await sl<CallService>()
|
|
|
|
|
final state = await runFriendly<Map<String, dynamic>>(
|
|
|
|
|
() => sl<CallService>()
|
|
|
|
|
.getCallState(_joinedChannel)
|
|
|
|
|
.timeout(const Duration(seconds: 3));
|
|
|
|
|
if (state?['status']?.toString() == 'ENDED') {
|
|
|
|
|
await _finishIncomingRemoteEnded();
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
.timeout(const Duration(seconds: 3)),
|
|
|
|
|
onError: (_) {},
|
|
|
|
|
fallback: 'Polling panggilan masuk gagal.',
|
|
|
|
|
);
|
|
|
|
|
if (state?['status']?.toString() == 'ENDED') {
|
|
|
|
|
await _finishIncomingRemoteEnded();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|