forked from eatvio/eatvio_app
feat(video_calls): added system ui for call and navigation to the app
This commit is contained in:
@@ -14,7 +14,6 @@ import flutter_foreground_task
|
||||
private var voipDiagnosticsChannel: FlutterMethodChannel?
|
||||
private let voipDiagnosticsStorageKey = "EatvioVoipDiagnostics"
|
||||
private let voipDiagnosticsChannelName = "com.eatvioapp/voip_diagnostics"
|
||||
private let appUrlScheme = "eatvio"
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
@@ -75,37 +74,6 @@ import flutter_foreground_task
|
||||
open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
if url.scheme == appUrlScheme {
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
let queryItems = components?.queryItems ?? []
|
||||
let conversationUuid = queryItems.first { $0.name == "conversationUuid" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let callKitId = queryItems.first { $0.name == "callKitId" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_url_opened",
|
||||
data: [
|
||||
"callKitId": callKitId ?? "",
|
||||
"hasCallKitId": callKitId != nil,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
]
|
||||
)
|
||||
if let callKitId, !callKitId.isEmpty, !conversationUuid.isEmpty {
|
||||
voipDiagnosticsChannel?.invokeMethod(
|
||||
"acceptedVoipCall",
|
||||
arguments: [
|
||||
"id": callKitId,
|
||||
"handle": conversationUuid,
|
||||
"extra": [
|
||||
"conversationUuid": conversationUuid
|
||||
],
|
||||
]
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return YXLSdk.shared.handleOpen(
|
||||
url,
|
||||
sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String
|
||||
@@ -140,75 +108,6 @@ import flutter_foreground_task
|
||||
return nil
|
||||
}
|
||||
|
||||
private func conversationUuid(fromCall call: Call) -> String? {
|
||||
let extra = call.data.extra as? [String: Any] ?? [:]
|
||||
for key in ["conversationUuid", "conversation_uuid", "conversation_id", "chat_uuid", "uuid"] {
|
||||
if let value = extra[key] as? String {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let handle = call.data.handle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !handle.isEmpty {
|
||||
return handle
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func openAppForAcceptedCall(_ call: Call) {
|
||||
guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else {
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_missing_conversation_uuid",
|
||||
data: ["callKitId": call.data.uuid]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var components = URLComponents()
|
||||
components.scheme = appUrlScheme
|
||||
components.host = "video-call"
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "callKitId", value: call.data.uuid),
|
||||
URLQueryItem(name: "conversationUuid", value: conversationUuid)
|
||||
]
|
||||
|
||||
guard let url = components.url else {
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_url_build_failed",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_open_app_requested",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
"appState": UIApplication.shared.applicationState.rawValue,
|
||||
]
|
||||
)
|
||||
|
||||
UIApplication.shared.open(url, options: [:]) { [weak self] success in
|
||||
self?.recordVoipDiagnostic(
|
||||
success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
|
||||
guard let value = value else {
|
||||
return ""
|
||||
@@ -446,18 +345,6 @@ import flutter_foreground_task
|
||||
|
||||
|
||||
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
|
||||
recordVoipDiagnostic(
|
||||
"callkit_accept_native",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid(fromCall: call) ?? "",
|
||||
"data": call.data.toJSON(),
|
||||
]
|
||||
)
|
||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON())
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.openAppForAcceptedCall(call)
|
||||
}
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
|
||||
@@ -91,14 +91,6 @@
|
||||
<string>yx5399b03636104c6d920b87c1ad329254</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Eatvio</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>eatvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -30,6 +30,7 @@ class VideoCallCallKitCoordinator {
|
||||
final VideoCallSessionCubit? _videoCallSessionCubit;
|
||||
|
||||
StreamSubscription<CallEvent?>? _eventSubscription;
|
||||
AppLifecycleListener? _lifecycleListener;
|
||||
final Map<String, CallKitParams> _callKitParamsById = {};
|
||||
final Set<String> _acceptHandledCallKitIds = {};
|
||||
final Set<String> _suppressedEndCallKitIds = {};
|
||||
@@ -49,16 +50,17 @@ class VideoCallCallKitCoordinator {
|
||||
|
||||
if (Platform.isIOS) {
|
||||
_voipDiagnosticsChannel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case 'voipDiagnosticsUpdated':
|
||||
_scheduleNativeVoipDiagnosticsDrain();
|
||||
case 'acceptedVoipCall':
|
||||
await _handleNativeAcceptedVoipCall(call.arguments);
|
||||
if (call.method == 'voipDiagnosticsUpdated') {
|
||||
_scheduleNativeVoipDiagnosticsDrain();
|
||||
}
|
||||
});
|
||||
await _firebaseApi.registerPushToken();
|
||||
_lifecycleListener = AppLifecycleListener(
|
||||
onResume: () => unawaited(_openActiveCallKitCallIfNeeded()),
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scheduleNativeVoipDiagnosticsDrain();
|
||||
unawaited(_openActiveCallKitCallIfNeeded());
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +83,9 @@ class VideoCallCallKitCoordinator {
|
||||
|
||||
void dispose() {
|
||||
unawaited(_eventSubscription?.cancel());
|
||||
_lifecycleListener?.dispose();
|
||||
_eventSubscription = null;
|
||||
_lifecycleListener = null;
|
||||
_callKitParamsById.clear();
|
||||
_initialized = false;
|
||||
}
|
||||
@@ -285,6 +289,53 @@ class VideoCallCallKitCoordinator {
|
||||
return _paramsForCallId(id);
|
||||
}
|
||||
|
||||
/// When the user opens the app from the system CallKit UI (e.g. taps "Итвио"),
|
||||
/// navigate to the in-app call screen for an already-accepted active call.
|
||||
Future<void> _openActiveCallKitCallIfNeeded() async {
|
||||
if (!Platform.isIOS) return;
|
||||
if (_cubit.state.connectPhase != VideoCallConnectPhase.idle) return;
|
||||
|
||||
final activeCalls = await FlutterCallkitIncoming.activeCalls();
|
||||
if (activeCalls.isEmpty) return;
|
||||
|
||||
for (final params in activeCalls) {
|
||||
_cacheCallKitParams(params);
|
||||
if (!params.isAccepted) continue;
|
||||
|
||||
final extra = params.extra ?? <String, dynamic>{};
|
||||
final conversationUuid = _parseConversationUuid(
|
||||
extra,
|
||||
handle: params.handle,
|
||||
);
|
||||
if (conversationUuid.isEmpty) continue;
|
||||
|
||||
await _deliverIncomingAccept(
|
||||
callKitId: params.id,
|
||||
conversationUuid: conversationUuid,
|
||||
counterpartName:
|
||||
_parseString(extra, const [
|
||||
'counterpartName',
|
||||
'caller_name',
|
||||
'callerName',
|
||||
]) ??
|
||||
params.nameCaller,
|
||||
counterpartAvatarUrl:
|
||||
_parseString(extra, const [
|
||||
'counterpartAvatarUrl',
|
||||
'avatar',
|
||||
'avatarUrl',
|
||||
'caller_avatar',
|
||||
]) ??
|
||||
params.avatar,
|
||||
localParticipantName: _parseString(extra, const [
|
||||
'localParticipantName',
|
||||
'local_participant_name',
|
||||
]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onNativeAcceptCallHandle(Map<dynamic, dynamic> body) async {
|
||||
final extra = Map<String, dynamic>.from(body);
|
||||
final conversationUuid = _parseConversationUuid(extra);
|
||||
@@ -317,57 +368,6 @@ class VideoCallCallKitCoordinator {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleNativeAcceptedVoipCall(Object? arguments) async {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
final data = _stringKeyedMap(arguments);
|
||||
final extra = _stringKeyedMap(data['extra']);
|
||||
final callKitId = _parseString(data, const ['id', 'uuid']);
|
||||
if (callKitId == null) {
|
||||
log(
|
||||
'VideoCallCallKitCoordinator: native acceptedVoipCall without callKitId, data=$data',
|
||||
name: 'VideoCallCallKit',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final conversationUuid = _parseConversationUuid(
|
||||
extra,
|
||||
handle: _parseString(data, const ['handle']),
|
||||
);
|
||||
if (conversationUuid.isEmpty) {
|
||||
log(
|
||||
'VideoCallCallKitCoordinator: native acceptedVoipCall without conversationUuid, data=$data',
|
||||
name: 'VideoCallCallKit',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deliverIncomingAccept(
|
||||
callKitId: callKitId,
|
||||
conversationUuid: conversationUuid,
|
||||
counterpartName:
|
||||
_parseString(extra, const [
|
||||
'counterpartName',
|
||||
'caller_name',
|
||||
'callerName',
|
||||
]) ??
|
||||
_parseString(data, const ['nameCaller']),
|
||||
counterpartAvatarUrl:
|
||||
_parseString(extra, const [
|
||||
'counterpartAvatarUrl',
|
||||
'avatar',
|
||||
'avatarUrl',
|
||||
'caller_avatar',
|
||||
]) ??
|
||||
_parseString(data, const ['avatar']),
|
||||
localParticipantName: _parseString(extra, const [
|
||||
'localParticipantName',
|
||||
'local_participant_name',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAccept(String callKitId) async {
|
||||
if (_acceptHandledCallKitIds.contains(callKitId)) return;
|
||||
|
||||
@@ -387,6 +387,14 @@ class VideoCallCallKitCoordinator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
log(
|
||||
'VideoCallCallKitCoordinator: iOS accept received (keeping CallKit UI), callKitId=$callKitId, conversationUuid=$conversationUuid',
|
||||
name: 'VideoCallCallKit',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deliverIncomingAccept(
|
||||
callKitId: callKitId,
|
||||
conversationUuid: conversationUuid,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.27+114
|
||||
version: 1.3.27+115
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user