forked from eatvio/eatvio_app
Compare commits
3 Commits
fix/testfl
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 194b8d5b16 | |||
|
|
508c321913 | ||
|
|
4a5c67632e |
@@ -437,73 +437,21 @@ import flutter_foreground_task
|
||||
return payload
|
||||
}
|
||||
|
||||
private func bringAppToForeground() {
|
||||
DispatchQueue.main.async {
|
||||
for scene in UIApplication.shared.connectedScenes {
|
||||
guard let windowScene = scene as? UIWindowScene else { continue }
|
||||
for window in windowScene.windows {
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
self.window?.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
)
|
||||
/// The answer callback runs before the scene is visible. A custom-scheme
|
||||
/// open from here is an untrusted user action and SpringBoard denies it.
|
||||
/// CallKit foregrounds a video call itself after `fulfill`. Flutter reads
|
||||
/// the stored payload once that scene is actually active.
|
||||
func deliverStoredAcceptedCall() {
|
||||
guard voipDiagnosticsChannel != nil else { return }
|
||||
guard let payload = UserDefaults.standard.dictionary(forKey: pendingAcceptedVoipCallKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
var components = URLComponents()
|
||||
components.scheme = appUrlScheme
|
||||
components.host = "video-call"
|
||||
var queryItems = [
|
||||
URLQueryItem(name: "callKitId", value: call.data.uuid),
|
||||
URLQueryItem(name: "conversationUuid", value: conversationUuid)
|
||||
]
|
||||
let serverCallId = extraServerCallId(call.data.extra)
|
||||
if !serverCallId.isEmpty {
|
||||
queryItems.append(URLQueryItem(name: "call_id", value: serverCallId))
|
||||
}
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_url_build_failed",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
]
|
||||
)
|
||||
let acceptedAt = (payload[pendingAcceptedAtKey] as? NSNumber)?.doubleValue
|
||||
if let acceptedAt, Date().timeIntervalSince1970 - acceptedAt > pendingAcceptedCallTtl {
|
||||
UserDefaults.standard.removeObject(forKey: pendingAcceptedVoipCallKey)
|
||||
return
|
||||
}
|
||||
|
||||
recordVoipDiagnostic(
|
||||
"accepted_call_open_app_requested",
|
||||
data: [
|
||||
"callKitId": call.data.uuid,
|
||||
"conversationUuid": conversationUuid,
|
||||
"url": url.absoluteString,
|
||||
"appState": UIApplication.shared.applicationState.rawValue,
|
||||
]
|
||||
)
|
||||
|
||||
bringAppToForeground()
|
||||
|
||||
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,
|
||||
]
|
||||
)
|
||||
}
|
||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: payload)
|
||||
}
|
||||
|
||||
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
|
||||
@@ -1177,6 +1125,8 @@ import flutter_foreground_task
|
||||
// activation and can start I/O before didActivate.
|
||||
prepareAvAudioSessionCategory()
|
||||
storePendingAcceptedCall(call)
|
||||
let extra = call.data.extra as? [String: Any] ?? [:]
|
||||
VideoCallDeclineClient.sendAccept(extra: extra, handle: call.data.handle)
|
||||
recordVoipDiagnostic(
|
||||
"callkit_accept_native",
|
||||
data: [
|
||||
@@ -1185,11 +1135,7 @@ import flutter_foreground_task
|
||||
"data": call.data.toJSON(),
|
||||
]
|
||||
)
|
||||
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON())
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.bringAppToForeground()
|
||||
self?.openAppForAcceptedCall(call)
|
||||
}
|
||||
deliverStoredAcceptedCall()
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ class SceneDelegate: FlutterSceneDelegate {
|
||||
override func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
super.sceneDidBecomeActive(scene)
|
||||
registerCallChannels()
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
|
||||
return
|
||||
}
|
||||
appDelegate.deliverStoredAcceptedCall()
|
||||
}
|
||||
|
||||
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||
|
||||
@@ -8,25 +8,37 @@ enum VideoCallDeclineClient {
|
||||
/// `POST /video-call/decline` from the CallKit callback. The process is
|
||||
/// awake here even when the user never opens the app afterwards.
|
||||
static func send(extra: [String: Any], handle: String) {
|
||||
post(action: "decline", extra: extra, handle: handle)
|
||||
}
|
||||
|
||||
/// `POST /video-call/accept` from the CallKit answer callback. Flutter may
|
||||
/// still be frozen on the splash snapshot, so the other device must be
|
||||
/// told from here. A later Dart retry with the same hw_id is idempotent.
|
||||
static func sendAccept(extra: [String: Any], handle: String) {
|
||||
post(action: "accept", extra: extra, handle: handle)
|
||||
}
|
||||
|
||||
private static func post(action: String, extra: [String: Any], handle: String) {
|
||||
let fields = declineFields(extra: extra, handle: handle)
|
||||
guard let request = makeRequest(
|
||||
baseUrl: prefsString("apiBaseUrl") ?? prefsString("customBaseUrl"),
|
||||
token: prefsString("token"),
|
||||
hwId: UIDevice.current.identifierForVendor?.uuidString,
|
||||
fields: fields
|
||||
fields: fields,
|
||||
action: action
|
||||
) else {
|
||||
NSLog(
|
||||
"[EatvioVoIP] decline POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
||||
"[EatvioVoIP] \(action) POST skipped conversation=\(fields.conversationUuid) ended=\(fields.endedPush)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let task = EatvioBackgroundTask()
|
||||
task.begin(name: "video-call-decline")
|
||||
task.begin(name: "video-call-\(action)")
|
||||
let urlTask = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||
let code = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||
NSLog(
|
||||
"[EatvioVoIP] decline POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
||||
"[EatvioVoIP] \(action) POST status=\(code) conversation=\(fields.conversationUuid) error=\(error?.localizedDescription ?? "-")"
|
||||
)
|
||||
task.end()
|
||||
}
|
||||
@@ -37,7 +49,8 @@ enum VideoCallDeclineClient {
|
||||
baseUrl: String?,
|
||||
token: String?,
|
||||
hwId: String?,
|
||||
fields: VideoCallDeclineFields
|
||||
fields: VideoCallDeclineFields,
|
||||
action: String = "decline"
|
||||
) -> URLRequest? {
|
||||
if fields.endedPush { return nil }
|
||||
let conversation = fields.conversationUuid.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -50,8 +63,9 @@ enum VideoCallDeclineClient {
|
||||
guard !conversation.isEmpty, !authToken.isEmpty else { return nil }
|
||||
guard deviceId.count >= 8, deviceId.count <= 64 else { return nil }
|
||||
guard root.hasPrefix("http://") || root.hasPrefix("https://") else { return nil }
|
||||
guard action == "accept" || action == "decline" else { return nil }
|
||||
guard let encoded = conversation.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
|
||||
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/decline")
|
||||
let url = URL(string: "\(root)/api/v2/chat/\(encoded)/video-call/\(action)")
|
||||
else { return nil }
|
||||
|
||||
var body: [String: String] = ["hw_id": deviceId]
|
||||
|
||||
@@ -52,6 +52,7 @@ class VideoCallCallKitCoordinator {
|
||||
|
||||
_PendingIncomingAccept? _pendingIncomingAccept;
|
||||
_AndroidAcceptResumeObserver? _androidResumeObserver;
|
||||
_AppResumeObserver? _iosResumeObserver;
|
||||
bool _pendingAcceptDrainScheduled = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
@@ -86,6 +87,12 @@ class VideoCallCallKitCoordinator {
|
||||
}
|
||||
});
|
||||
await _firebaseApi.registerPushToken();
|
||||
_iosResumeObserver = _AppResumeObserver(
|
||||
onResumed: () {
|
||||
unawaited(_drainPendingNativeAcceptedVoipCall());
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_iosResumeObserver!);
|
||||
await _drainPendingNativeAcceptedVoipCall();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scheduleNativeVoipDiagnosticsDrain();
|
||||
@@ -257,6 +264,11 @@ class VideoCallCallKitCoordinator {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
_androidResumeObserver = null;
|
||||
}
|
||||
final iosObserver = _iosResumeObserver;
|
||||
if (iosObserver != null) {
|
||||
WidgetsBinding.instance.removeObserver(iosObserver);
|
||||
_iosResumeObserver = null;
|
||||
}
|
||||
unawaited(_eventSubscription?.cancel());
|
||||
_eventSubscription = null;
|
||||
_callKitParamsById.clear();
|
||||
@@ -1228,6 +1240,19 @@ class _PendingIncomingAccept {
|
||||
final bool? counterpartIsConsultant;
|
||||
}
|
||||
|
||||
class _AppResumeObserver with WidgetsBindingObserver {
|
||||
_AppResumeObserver({required this.onResumed});
|
||||
|
||||
final VoidCallback onResumed;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
onResumed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidAcceptResumeObserver with WidgetsBindingObserver {
|
||||
_AndroidAcceptResumeObserver({required this.onResumed});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.68+157
|
||||
version: 1.3.69+158
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user