Skip to content

Commit

Permalink
Handle notifications properly when a call is happening. (#3276)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixlwave authored Sep 13, 2024
1 parent ce83aec commit 93daaf5
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 11 deletions.
2 changes: 2 additions & 0 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 19 additions & 5 deletions ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
}
Expand All @@ -623,12 +625,24 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
analytics.track(screen: .RoomCall)
}

private func hideCallScreenOverlay() {
guard let callScreenPictureInPictureController else {
MXLog.warning("Picture in picture isn't available, dismissing the call screen.")
dismissCallScreenIfNeeded()
return
}

MXLog.info("Starting picture in picture to hide the call screen overlay.")
callScreenPictureInPictureController.startPictureInPicture()
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
}

private func dismissCallScreenIfNeeded() {
guard navigationSplitCoordinator.sheetCoordinator is CallScreenCoordinator else {
guard navigationSplitCoordinator.overlayCoordinator is CallScreenCoordinator else {
return
}

navigationSplitCoordinator.setSheetCoordinator(nil)
navigationSplitCoordinator.setOverlayCoordinator(nil)
}

// MARK: Secure backup confirmation
Expand Down
15 changes: 11 additions & 4 deletions ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion ElementX/Sources/Screens/CallScreen/CallScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import AVKit
import Foundation

enum CallScreenViewModelAction {
case pictureInPictureStarted(AVPictureInPictureController)
case pictureInPictureIsAvailable(AVPictureInPictureController)
case pictureInPictureStarted
case pictureInPictureStopped
case dismiss
}
Expand All @@ -34,6 +35,7 @@ struct Bindings {

enum CallScreenViewAction {
case urlChanged(URL?)
case pictureInPictureIsAvailable(AVPictureInPictureController)
case navigateBack
case pictureInPictureWillStop
case endCall
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
Expand Down
13 changes: 13 additions & 0 deletions ElementX/Sources/Screens/CallScreen/View/CallScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ private struct CallView: UIViewRepresentable {
contentViewController: pictureInPictureViewController))
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
viewModelContext.send(viewAction: .pictureInPictureIsAvailable(pictureInPictureController))
}
}

Expand Down Expand Up @@ -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) }
}
Expand Down

0 comments on commit 93daaf5

Please sign in to comment.