From 93daaf554f5383e68ea4df2ff2e044b149160316 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:50:29 +0100 Subject: [PATCH] Handle notifications properly when a call is happening. (#3276) --- .../Sources/Application/AppCoordinator.swift | 2 ++ .../UserSessionFlowCoordinator.swift | 26 ++++++++++++++----- .../CallScreen/CallScreenCoordinator.swift | 15 ++++++++--- .../Screens/CallScreen/CallScreenModels.swift | 4 ++- .../CallScreen/CallScreenViewModel.swift | 4 ++- .../Screens/CallScreen/View/CallScreen.swift | 13 ++++++++++ 6 files changed, 52 insertions(+), 12 deletions(-) diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index f03d7d84e..7b959bd65 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -643,6 +643,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg .sink { [weak self] action in guard let self else { return } switch action { + case .pictureInPictureIsAvailable: + break case .pictureInPictureStarted, .pictureInPictureStopped: // Don't allow PiP when signed out - the user could login at which point we'd // need to hand over the call from here to the user session flow coordinator. diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 02b0267fc..a352e2a3f 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -319,6 +319,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } else { Task { await self.startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated) } } + hideCallScreenOverlay() // Turn any active call into a PiP so that navigation from a notification is visible to the user. case(.roomList, .deselectRoom, .roomList): dismissRoomFlow(animated: animated) @@ -604,15 +605,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { .sink { [weak self] action in guard let self else { return } switch action { - case .pictureInPictureStarted(let controller): - MXLog.info("Hiding call for PiP presentation.") + case .pictureInPictureIsAvailable(let controller): callScreenPictureInPictureController = controller + case .pictureInPictureStarted: + MXLog.info("Hiding call for PiP presentation.") navigationSplitCoordinator.setOverlayPresentationMode(.minimized) case .pictureInPictureStopped: MXLog.info("Restoring call after PiP presentation.") navigationSplitCoordinator.setOverlayPresentationMode(.fullScreen) - callScreenPictureInPictureController = nil case .dismiss: + callScreenPictureInPictureController = nil navigationSplitCoordinator.setOverlayCoordinator(nil) } } @@ -623,12 +625,24 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { analytics.track(screen: .RoomCall) } - private func dismissCallScreenIfNeeded() { - guard navigationSplitCoordinator.sheetCoordinator is CallScreenCoordinator else { + private func hideCallScreenOverlay() { + guard let callScreenPictureInPictureController else { + MXLog.warning("Picture in picture isn't available, dismissing the call screen.") + dismissCallScreenIfNeeded() return } - navigationSplitCoordinator.setSheetCoordinator(nil) + MXLog.info("Starting picture in picture to hide the call screen overlay.") + callScreenPictureInPictureController.startPictureInPicture() + navigationSplitCoordinator.setOverlayPresentationMode(.minimized) + } + + private func dismissCallScreenIfNeeded() { + guard navigationSplitCoordinator.overlayCoordinator is CallScreenCoordinator else { + return + } + + navigationSplitCoordinator.setOverlayCoordinator(nil) } // MARK: Secure backup confirmation diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift index 7758aad92..bc9a65ce3 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift @@ -17,8 +17,13 @@ struct CallScreenCoordinatorParameters { } enum CallScreenCoordinatorAction { - /// The call is still ongoing but the user wishes to navigate around the app. - case pictureInPictureStarted(AVPictureInPictureController) + /// The call is able to be minimised to picture in picture with the provided controller. + /// + /// **Note:** Manually starting the PiP will not trigger the action below as we don't want + /// to change the app's navigation when backgrounding the app with the call screen visible. + case pictureInPictureIsAvailable(AVPictureInPictureController) + /// The call is still ongoing but the user requested to navigate around the app. + case pictureInPictureStarted /// The call is hidden and the user wishes to return to it. case pictureInPictureStopped /// The call is finished and the screen is done with. @@ -46,8 +51,10 @@ final class CallScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .pictureInPictureStarted(let controller): - actionsSubject.send(.pictureInPictureStarted(controller)) + case .pictureInPictureIsAvailable(let controller): + actionsSubject.send(.pictureInPictureIsAvailable(controller)) + case .pictureInPictureStarted: + actionsSubject.send(.pictureInPictureStarted) case .pictureInPictureStopped: actionsSubject.send(.pictureInPictureStopped) case .dismiss: diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift b/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift index e50ab065d..30a77891d 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift @@ -9,7 +9,8 @@ import AVKit import Foundation enum CallScreenViewModelAction { - case pictureInPictureStarted(AVPictureInPictureController) + case pictureInPictureIsAvailable(AVPictureInPictureController) + case pictureInPictureStarted case pictureInPictureStopped case dismiss } @@ -34,6 +35,7 @@ struct Bindings { enum CallScreenViewAction { case urlChanged(URL?) + case pictureInPictureIsAvailable(AVPictureInPictureController) case navigateBack case pictureInPictureWillStop case endCall diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index fb11ffebf..335b56acf 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -111,6 +111,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol case .urlChanged(let url): guard let url else { return } MXLog.info("URL changed to: \(url)") + case .pictureInPictureIsAvailable(let controller): + actionsSubject.send(.pictureInPictureIsAvailable(controller)) case .navigateBack: Task { await handleBackwardsNavigation() } case .pictureInPictureWillStop: @@ -188,7 +190,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol switch await requestPictureInPictureHandler() { case .success(let controller): - actionsSubject.send(.pictureInPictureStarted(controller)) + actionsSubject.send(.pictureInPictureStarted) case .failure: actionsSubject.send(.dismiss) } diff --git a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift index 395b1cfdd..11467ed48 100644 --- a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift +++ b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift @@ -128,6 +128,7 @@ private struct CallView: UIViewRepresentable { contentViewController: pictureInPictureViewController)) pictureInPictureController.delegate = self self.pictureInPictureController = pictureInPictureController + viewModelContext.send(viewAction: .pictureInPictureIsAvailable(pictureInPictureController)) } } @@ -222,6 +223,18 @@ private struct CallView: UIViewRepresentable { } } + nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + Task { @MainActor in + // Double check that the controller is definitely showing a page that supports picture in picture. + // This is necessary as it doesn't get checked when backgrounding the app or tapping a notification. + guard case .success(true) = await webViewCanEnterPictureInPicture() else { + MXLog.error("Picture in picture started on a webpage that doesn't support it. Ending the call.") + viewModelContext?.send(viewAction: .endCall) + return + } + } + } + nonisolated func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { Task { await viewModelContext?.send(viewAction: .pictureInPictureWillStop) } }