Restyle return to call view

This commit is contained in:
Nora Trapp 2020-08-14 17:59:09 -07:00
parent 7f935a2f0e
commit dc543ce24f
9 changed files with 279 additions and 272 deletions

View File

@ -31,6 +31,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
// MARK: - Views
private lazy var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
private lazy var backgroundAvatarView = UIImageView()
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
@ -190,6 +191,13 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
self.shouldUseTheme = false
}
deinit {
// These views might be in the return to call PIP's hierarchy,
// we want to remove them so they are free'd when the call ends
remoteVideoView.removeFromSuperview()
localVideoView.removeFromSuperview()
}
@objc func didBecomeActive() {
if self.isViewLoaded {
shouldRemoteVideoControlsBeHidden = false
@ -213,6 +221,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
override func loadView() {
view = UIView()
view.clipsToBounds = true
view.backgroundColor = UIColor.black
view.layoutMargins = UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20)
@ -271,8 +280,6 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
backgroundAvatarView.autoPinEdgesToSuperviewEdges()
// Dark blurred background.
let blurEffect = UIBlurEffect(style: .dark)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.isUserInteractionEnabled = false
view.addSubview(blurView)
blurView.autoPinEdgesToSuperviewEdges()
@ -587,11 +594,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
private func nearestValidLocalVideoFrame(for origin: CGPoint) -> CGRect {
var newFrame = CGRect(
origin: origin,
// The PIP has a fixed size for iPhone and iPad
size: CGSize(
width: UIDevice.current.isIPad ? 272 : 90,
height: UIDevice.current.isIPad ? 154 : 160
)
size: ReturnToCallViewController.pipSize
)
let boundingRect = localVideoBoundingRect
@ -631,6 +634,8 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
private func updateLocalVideoLayout() {
guard localVideoView.superview == view else { return }
guard !isRenderingLocalVanityVideo else {
view.layoutIfNeeded()
localVideoView.frame = view.frame
@ -1431,3 +1436,57 @@ extension CallViewController: UIGestureRecognizerDelegate {
}
}
}
extension CallViewController: CallViewControllerWindowReference {
var remoteVideoViewReference: UIView { remoteVideoView }
var localVideoViewReference: UIView { localVideoView }
@objc
public func returnFromPip(pipWindow: UIWindow) {
// The call "pip" uses our remote and local video views since only
// one `AVCaptureVideoPreviewLayer` per capture session is supported.
// We need to re-add them when we return to this view.
guard remoteVideoView.superview != view && localVideoView.superview != view else {
return owsFailDebug("unexpectedly returned to call while we own the video views")
}
guard let splitViewSnapshot = SignalApp.shared().snapshotSplitViewController(afterScreenUpdates: false) else {
return owsFailDebug("failed to snapshot rootViewController")
}
guard let pipSnapshot = pipWindow.snapshotView(afterScreenUpdates: false) else {
return owsFailDebug("failed to snapshot pip")
}
view.insertSubview(remoteVideoView, aboveSubview: blurView)
remoteVideoView.autoPinEdgesToSuperviewEdges()
view.insertSubview(localVideoView, aboveSubview: contactAvatarContainerView)
shouldRemoteVideoControlsBeHidden = false
animateReturnFromPip(pipSnapshot: pipSnapshot, pipFrame: pipWindow.frame, splitViewSnapshot: splitViewSnapshot)
}
private func animateReturnFromPip(pipSnapshot: UIView, pipFrame: CGRect, splitViewSnapshot: UIView) {
guard let window = view.window else { return owsFailDebug("missing window") }
view.superview?.insertSubview(splitViewSnapshot, belowSubview: view)
splitViewSnapshot.autoPinEdgesToSuperviewEdges()
view.frame = pipFrame
view.addSubview(pipSnapshot)
pipSnapshot.autoPinEdgesToSuperviewEdges()
view.layoutIfNeeded()
UIView.animate(withDuration: 0.2, animations: {
pipSnapshot.alpha = 0
self.view.frame = window.frame
self.view.layoutIfNeeded()
}) { _ in
self.updateCallUI()
splitViewSnapshot.removeFromSuperview()
pipSnapshot.removeFromSuperview()
}
}
}

View File

@ -55,6 +55,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)receivedVerificationCode:(NSString *)verificationCode;
- (void)applicationWillTerminate;
- (nullable UIView *)snapshotSplitViewControllerAfterScreenUpdates:(BOOL)afterScreenUpdates;
@end
NS_ASSUME_NONNULL_END

View File

@ -365,6 +365,11 @@ NSString *const kNSUserDefaults_DidTerminateKey = @"kNSUserDefaults_DidTerminate
[self.conversationSplitViewController showNewConversationView];
}
- (nullable UIView *)snapshotSplitViewControllerAfterScreenUpdates:(BOOL)afterScreenUpdates
{
return [self.conversationSplitViewController.view snapshotViewAfterScreenUpdates:afterScreenUpdates];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -7,7 +7,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface UINavigationController (OWSNavigationController) <UINavigationBarDelegate, NavBarLayoutDelegate>
@interface UINavigationController (OWSNavigationController) <UINavigationBarDelegate>
@end
@ -30,7 +30,11 @@ NS_ASSUME_NONNULL_BEGIN
if (!self) {
return self;
}
[self setupNavbar];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(themeDidChange:)
name:ThemeDidChangeNotification
object:nil];
return self;
}
@ -77,24 +81,6 @@ NS_ASSUME_NONNULL_BEGIN
return [super prefersStatusBarHidden];
}
#pragma mark - UINavigationBarDelegate
- (void)setupNavbar
{
if (![self.navigationBar isKindOfClass:[OWSNavigationBar class]]) {
OWSFailDebug(@"navigationBar was unexpected class: %@", self.navigationBar);
return;
}
OWSNavigationBar *navbar = (OWSNavigationBar *)self.navigationBar;
navbar.navBarLayoutDelegate = self;
[self updateLayoutForNavbar:navbar];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(themeDidChange:)
name:ThemeDidChangeNotification
object:nil];
}
// All OWSNavigationController serve as the UINavigationBarDelegate for their navbar.
// We override shouldPopItem: in order to cancel some back button presses - for example,
// if a view has unsaved changes.
@ -148,21 +134,10 @@ NS_ASSUME_NONNULL_BEGIN
}
}
#pragma mark - NavBarLayoutDelegate
- (void)navBarCallLayoutDidChangeWithNavbar:(OWSNavigationBar *)navbar
{
[self updateLayoutForNavbar:navbar];
[self setNeedsStatusBarAppearanceUpdate];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
if (!CurrentAppContext().isMainApp) {
return super.preferredStatusBarStyle;
} else if (OWSWindowManager.sharedManager.hasCall) {
// Status bar is overlaying the green "call banner"
return UIStatusBarStyleLightContent;
} else {
UIViewController *presentedViewController = self.presentedViewController;
if (presentedViewController != nil && !presentedViewController.isBeingDismissed) {
@ -173,26 +148,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)updateLayoutForNavbar:(OWSNavigationBar *)navbar
{
OWSLogDebug(@"");
[UIView setAnimationsEnabled:NO];
if (!CurrentAppContext().isMainApp) {
self.additionalSafeAreaInsets = UIEdgeInsetsZero;
} else if (OWSWindowManager.sharedManager.hasCall) {
self.additionalSafeAreaInsets = UIEdgeInsetsMake(20, 0, 0, 0);
} else {
self.additionalSafeAreaInsets = UIEdgeInsetsZero;
}
// we have to ensure the navbar frame *in* layoutSubviews.
[navbar layoutSubviews];
[UIView setAnimationsEnabled:YES];
}
#pragma mark - Orientation
- (UIInterfaceOrientationMask)supportedInterfaceOrientations

View File

@ -1,9 +1,19 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public protocol CallViewControllerWindowReference: class {
var localVideoViewReference: UIView { get }
var remoteVideoViewReference: UIView { get }
var thread: TSContactThread { get }
var view: UIView { get }
func returnFromPip(pipWindow: UIWindow)
}
@objc
public protocol ReturnToCallViewControllerDelegate: class {
func returnToCallWasTapped(_ viewController: ReturnToCallViewController)
@ -12,79 +22,207 @@ public protocol ReturnToCallViewControllerDelegate: class {
@objc
public class ReturnToCallViewController: UIViewController {
@objc
public static var pipSize = UIDevice.current.isIPad ? CGSize(width: 272, height: 204) : CGSize(width: 90, height: 160)
@objc
public weak var delegate: ReturnToCallViewControllerDelegate?
let returnToCallLabel = UILabel()
private weak var callViewController: CallViewControllerWindowReference?
@objc
public func startAnimating() {
NotificationCenter.default.addObserver(self, selector: #selector(didTapStatusBar(notification:)), name: .TappedStatusBar, object: nil)
self.returnToCallLabel.layer.removeAllAnimations()
self.returnToCallLabel.alpha = 1
UIView.animate(withDuration: 1,
delay: 0,
options: [.repeat, .autoreverse],
animations: { self.returnToCallLabel.alpha = 0 },
completion: { _ in self.returnToCallLabel.alpha = 1 })
public func displayForCallViewController(_ callViewController: CallViewControllerWindowReference) {
guard callViewController !== self.callViewController else { return }
guard let callViewSnapshot = callViewController.view.snapshotView(afterScreenUpdates: false) else {
return owsFailDebug("failed to snapshot call view")
}
self.callViewController = callViewController
view.addSubview(callViewController.remoteVideoViewReference)
callViewController.remoteVideoViewReference.autoPinEdgesToSuperviewEdges()
view.addSubview(callViewController.localVideoViewReference)
callViewController.localVideoViewReference.layer.cornerRadius = 6
let localVideoSize = CGSizeScale(Self.pipSize, 0.3)
callViewController.localVideoViewReference.frame = CGRect(
origin: CGPoint(
x: Self.pipSize.width - 6 - localVideoSize.width,
y: Self.pipSize.height - 6 - localVideoSize.height
),
size: localVideoSize
)
backgroundAvatarView.image = Environment.shared.contactsManager.profileImageForAddress(
withSneakyTransaction: callViewController.thread.contactAddress
)
avatarView.image = OWSAvatarBuilder.buildImage(thread: callViewController.thread, diameter: 60)
animatePipPresentation(snapshot: callViewSnapshot)
}
@objc
public func stopAnimating() {
NotificationCenter.default.removeObserver(self, name: .TappedStatusBar, object: nil)
self.returnToCallLabel.layer.removeAllAnimations()
}
private lazy var avatarView = AvatarImageView()
private lazy var backgroundAvatarView = UIImageView()
private lazy var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
override public func loadView() {
self.view = UIView()
view = UIView()
// This is the color of the iOS "return to call" banner.
view.backgroundColor = UIColor(rgbHex: 0x4cd964)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
view.addGestureRecognizer(tapGesture)
view.backgroundColor = .black
view.clipsToBounds = true
view.layer.cornerRadius = 8
view.addSubview(returnToCallLabel)
backgroundAvatarView.contentMode = .scaleAspectFill
view.addSubview(backgroundAvatarView)
backgroundAvatarView.autoPinEdgesToSuperviewEdges()
// System UI doesn't use dynamic type for status bar; neither do we.
returnToCallLabel.font = UIFont.ows_regularFont(withSize: 14)
returnToCallLabel.text = NSLocalizedString("CALL_WINDOW_RETURN_TO_CALL", comment: "Label for the 'return to call' banner.")
returnToCallLabel.textColor = .white
view.addSubview(blurView)
blurView.autoPinEdgesToSuperviewEdges()
returnToCallLabel.autoPinEdge(toSuperviewEdge: .bottom, withInset: 2)
returnToCallLabel.setCompressionResistanceHigh()
returnToCallLabel.setContentHuggingHigh()
returnToCallLabel.autoHCenterInSuperview()
view.addSubview(avatarView)
avatarView.autoSetDimensions(to: CGSize(square: 60))
avatarView.autoCenterInSuperview()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan)))
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
OWSWindowManager.shared.ensureReturnToCallWindowFrame()
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.updatePipLayout()
}, completion: nil)
}
@objc
public func didTapView(gestureRecognizer: UITapGestureRecognizer) {
self.delegate?.returnToCallWasTapped(self)
// MARK: -
private func animatePipPresentation(snapshot: UIView) {
guard let window = view.window else { return owsFailDebug("missing window") }
let previousOrigin = window.frame.origin
window.frame = OWSWindowManager.shared.rootWindow.bounds
view.addSubview(snapshot)
snapshot.autoPinEdgesToSuperviewEdges()
window.layoutIfNeeded()
UIView.animate(withDuration: 0.2, animations: {
snapshot.alpha = 0
window.frame = self.nearestValidPipFrame(for: previousOrigin)
window.layoutIfNeeded()
}) { _ in
snapshot.removeFromSuperview()
}
}
private var pipBoundingRect: CGRect {
let padding: CGFloat = 4
var rect = CurrentAppContext().frame
let safeAreaInsets = OWSWindowManager.shared.rootWindow.safeAreaInsets
let leftInset = safeAreaInsets.left + padding
let rightInset = safeAreaInsets.right + padding
rect.origin.x += leftInset
rect.size.width -= leftInset + rightInset
let topInset = safeAreaInsets.top + padding
let bottomInset = safeAreaInsets.bottom + padding
rect.origin.y += topInset
rect.size.height -= topInset + bottomInset
return rect
}
private func nearestValidPipFrame(for origin: CGPoint) -> CGRect {
var newFrame = CGRect(origin: origin, size: Self.pipSize)
let boundingRect = pipBoundingRect
// If the origin is zero, we always want to position
// the pip in the top right
let hasZeroOrigin = newFrame.origin == .zero
// If we're positioned outside of the vertical bounds, we
// want to position the pip at the nearest bound
let positionedOutOfVerticalBounds = newFrame.minY < boundingRect.minY || newFrame.maxY > boundingRect.maxY
// If we're position anywhere but exactly at the horizontal
// edges, we want to position the pip at the nearest edge
let positionedAwayFromHorizontalEdges = boundingRect.minX != newFrame.minX && boundingRect.maxX != newFrame.maxX
if positionedOutOfVerticalBounds {
if newFrame.minY < boundingRect.minY || hasZeroOrigin {
newFrame.origin.y = boundingRect.minY
} else {
newFrame.origin.y = boundingRect.maxY - newFrame.height
}
}
if positionedAwayFromHorizontalEdges {
let distanceFromLeading = newFrame.minX - boundingRect.minX
let distanceFromTrailing = boundingRect.maxX - newFrame.maxX
if distanceFromLeading > distanceFromTrailing || hasZeroOrigin {
newFrame.origin.x = boundingRect.maxX - newFrame.width
} else {
newFrame.origin.x = boundingRect.minX
}
}
return newFrame
}
private func updatePipLayout() {
guard let window = view.window else { return owsFailDebug("missing window") }
let newFrame = nearestValidPipFrame(for: window.frame.origin)
UIView.animate(withDuration: 0.25) { window.frame = newFrame }
}
private var startingTranslation: CGPoint?
@objc func handlePan(sender: UIPanGestureRecognizer) {
guard let window = view.window else { return owsFailDebug("missing window") }
switch sender.state {
case .began, .changed:
let translation = sender.translation(in: view)
sender.setTranslation(.zero, in: view)
window.frame.origin.y += translation.y
window.frame.origin.x += translation.x
case .ended, .cancelled, .failed:
let velocity = sender.velocity(in: view)
// TODO: maybe do more sophisticated deceleration
let duration: CGFloat = 0.35
let additionalDistanceX = velocity.x * duration
let additionalDistanceY = velocity.y * duration
let finalDestination = CGPoint(
x: window.frame.origin.x + additionalDistanceX,
y: window.frame.origin.y + additionalDistanceY
)
let finalFrame = nearestValidPipFrame(for: finalDestination)
UIView.animate(withDuration: TimeInterval(duration)) { window.frame = finalFrame }
default:
break
}
}
@objc
public func didTapStatusBar(notification: Notification) {
private func handleTap(sender: UITapGestureRecognizer) {
callViewController?.localVideoViewReference.removeFromSuperview()
callViewController?.remoteVideoViewReference.removeFromSuperview()
callViewController = nil
self.delegate?.returnToCallWasTapped(self)
}
override public func viewWillLayoutSubviews() {
Logger.debug("")
super.viewWillLayoutSubviews()
}
override public func viewDidLayoutSubviews() {
Logger.debug("")
super.viewDidLayoutSubviews()
}
// MARK: Orientation
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {

View File

@ -37,7 +37,6 @@ public class StickerPackViewController: OWSViewController {
stickerCollectionView.show(dataSource: dataSource)
dataSource.add(delegate: self)
NotificationCenter.default.addObserver(self, selector: #selector(callDidChange), name: .OWSWindowManagerCallDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didChangeStatusBarFrame), name: UIApplication.didChangeStatusBarFrameNotification, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(stickersOrPacksDidChange),
@ -409,13 +408,6 @@ public class StickerPackViewController: OWSViewController {
StickerSharingViewController.shareStickerPack(stickerPack.info, from: self)
}
@objc
public func callDidChange() {
Logger.debug("")
updateContent()
}
@objc
public func didChangeStatusBarFrame() {
Logger.debug("")

View File

@ -5,25 +5,12 @@
import Foundation
import UIKit
@objc
public protocol NavBarLayoutDelegate: class {
func navBarCallLayoutDidChange(navbar: OWSNavigationBar)
}
@objc
public class OWSNavigationBar: UINavigationBar {
@objc
public weak var navBarLayoutDelegate: NavBarLayoutDelegate?
@objc
public let navbarWithoutStatusHeight: CGFloat = 44
@objc
public var callBannerHeight: CGFloat {
return OWSWindowManagerCallBannerHeight()
}
@objc
public var statusBarHeight: CGFloat {
return CurrentAppContext().statusBarHeight
@ -47,8 +34,6 @@ public class OWSNavigationBar: UINavigationBar {
applyTheme()
NotificationCenter.default.addObserver(self, selector: #selector(callDidChange), name: .OWSWindowManagerCallDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didChangeStatusBarFrame), name: UIApplication.didChangeStatusBarFrameNotification, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(themeDidChange),
name: .ThemeDidChange,
@ -110,36 +95,6 @@ public class OWSNavigationBar: UINavigationBar {
}
}
public func snapshotViewIncludingBackground(afterScreenUpdates: Bool) -> UIView? {
let originalFrame = self.frame
let originalBounds = self.bounds
// NavigationBars are weird because though it appears as though the status bar
// content is "in" or maybe "above" (z-index) the navbar, the navbar frame is strictly
// lower (y-index) than the status bar.
// To work with that, the navbar background, including the blur effect when transparency
// is enabled, extends beyond the navbars's bounds. This allows the background to extend
// up (y-index) and under (z-index) the status bar, without clips to bounds.
//
// Snapshots, however, clip to bounds. So as to capture the full size of the background
// in our snapshot we temporarily adjust the navbars frame.
self.frame = CGRect(x: 0, y: callBannerHeight, width: fullWidth, height: navbarWithoutStatusHeight + statusBarHeight)
self.bounds = self.frame
defer {
self.frame = originalFrame
self.bounds = originalBounds
}
guard let barSnapshot = self.snapshotView(afterScreenUpdates: afterScreenUpdates) else {
owsFailDebug("barSnapshot was unexpectedly nil")
return nil
}
barSnapshot.frame.origin = .zero
return barSnapshot
}
@objc
public func themeDidChange() {
Logger.debug("")
@ -153,60 +108,6 @@ public class OWSNavigationBar: UINavigationBar {
}
}
// MARK: Layout
@objc
public func callDidChange() {
Logger.debug("")
self.navBarLayoutDelegate?.navBarCallLayoutDidChange(navbar: self)
}
@objc
public func didChangeStatusBarFrame() {
Logger.debug("")
self.navBarLayoutDelegate?.navBarCallLayoutDidChange(navbar: self)
}
public override func layoutSubviews() {
guard CurrentAppContext().isMainApp else {
super.layoutSubviews()
return
}
guard OWSWindowManager.shared.hasCall else {
super.layoutSubviews()
return
}
super.layoutSubviews()
self.frame = CGRect(x: 0, y: callBannerHeight, width: fullWidth, height: navbarWithoutStatusHeight)
self.bounds = CGRect(x: 0, y: 0, width: fullWidth, height: navbarWithoutStatusHeight)
// This is only necessary on iOS11, which has some private views within that lay outside of the navbar.
// They aren't actually visible behind the call status bar, but they looks strange during present/dismiss
// animations for modal VC's
for subview in self.subviews {
let stringFromClass = NSStringFromClass(subview.classForCoder)
if stringFromClass.contains("BarBackground") {
subview.frame = self.bounds
} else if stringFromClass.contains("BarContentView") {
subview.frame = self.bounds
}
}
}
public override func didMoveToSuperview() {
super.didMoveToSuperview()
// `fullWidth` will not be accurate until the navbar has a defined
// superview. By forcing a re-layout after moving to a superview, we
// ensure that the correct width is always used. This works around a bug
// on iPad where the navigation bar is larger than the application frame
// when in slide over mode.
setNeedsLayout()
layoutIfNeeded()
}
// MARK: Override Theme
@objc

View File

@ -16,10 +16,10 @@ extern NSString *const IsScreenBlockActiveDidChangeNotification;
#pragma mark -
const CGFloat OWSWindowManagerCallBannerHeight(void);
extern const UIWindowLevel UIWindowLevel_Background;
@protocol CallViewControllerWindowReference;
@interface OWSWindowManager : NSObject
+ (instancetype)new NS_UNAVAILABLE;
@ -36,15 +36,14 @@ extern const UIWindowLevel UIWindowLevel_Background;
- (BOOL)isAppWindow:(UIWindow *)window;
- (void)updateWindowFrames;
- (void)ensureReturnToCallWindowFrame;
#pragma mark - Calls
@property (nonatomic, readonly) BOOL shouldShowCallView;
@property (nonatomic, readonly) UIWindow *callViewWindow;
- (void)startCall:(UIViewController *)callViewController;
- (void)endCall:(UIViewController *)callViewController;
- (void)startCall:(UIViewController<CallViewControllerWindowReference> *)callViewController;
- (void)endCall:(UIViewController<CallViewControllerWindowReference> *)callViewController;
- (void)leaveCallView;
@property (nonatomic, readonly) BOOL hasCall;

View File

@ -14,26 +14,6 @@ NSString *const OWSWindowManagerCallDidChangeNotification = @"OWSWindowManagerCa
NSString *const IsScreenBlockActiveDidChangeNotification = @"IsScreenBlockActiveDidChangeNotification";
const CGFloat OWSWindowManagerCallBannerHeight(void)
{
if (@available(iOS 11.4, *)) {
return CurrentAppContext().statusBarHeight + 20;
}
if (![UIDevice currentDevice].hasIPhoneXNotch) {
return CurrentAppContext().statusBarHeight + 20;
}
// Hardcode CallBanner height for iPhone X's on older iOS.
//
// As of iOS11.4 and iOS12, this no longer seems to be an issue, but previously statusBarHeight returned
// something like 20pts (IIRC), meaning our call banner did not extend sufficiently past the iPhone X notch.
//
// Before noticing that this behavior changed, I actually assumed that notch height was intentionally excluded from
// the statusBarHeight, and that this was not a bug, else I'd have taken better notes.
return 64;
}
// Behind everything, especially the root window.
const UIWindowLevel UIWindowLevel_Background = -1.f;
@ -110,7 +90,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
@property (nonatomic) BOOL shouldShowCallView;
@property (nonatomic, nullable) UIViewController *callViewController;
@property (nonatomic, nullable) UIViewController<CallViewControllerWindowReference> *callViewController;
@end
@ -153,39 +133,15 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
self.returnToCallWindow = [self createReturnToCallWindow:rootWindow];
self.callViewWindow = [self createCallViewWindow:rootWindow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangeStatusBarFrame:)
name:UIApplicationDidChangeStatusBarFrameNotification
object:nil];
[self ensureWindowState];
}
- (void)didChangeStatusBarFrame:(NSNotification *)notification
{
// Apple bug? Upon returning from landscape, this method *is* fired, but both the notification and [UIApplication
// sharedApplication].statusBarFrame still show a height of 0. So to work around we also call
// `ensureReturnToCallWindowFrame` before showing the call banner.
[self ensureReturnToCallWindowFrame];
}
- (void)ensureReturnToCallWindowFrame
{
CGRect newFrame = CurrentAppContext().frame;
newFrame.size.height = OWSWindowManagerCallBannerHeight();
OWSLogDebug(@"returnToCallWindowFrame: %@", NSStringFromCGRect(newFrame));
self.returnToCallWindow.frame = newFrame;
}
- (UIWindow *)createReturnToCallWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssertDebug(rootWindow);
// "Return to call" should remain at the top of the screen.
CGRect applicationFrame = CurrentAppContext().frame;
applicationFrame.size.height = OWSWindowManagerCallBannerHeight();
UIWindow *window = [[OWSWindow alloc] initWithFrame:applicationFrame];
UIWindow *window = [[OWSWindow alloc] initWithFrame:rootWindow.bounds];
window.hidden = YES;
window.windowLevel = UIWindowLevel_ReturnToCall();
window.opaque = YES;
@ -266,7 +222,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
#pragma mark - Calls
- (void)setCallViewController:(nullable UIViewController *)callViewController
- (void)setCallViewController:(nullable UIViewController<CallViewControllerWindowReference> *)callViewController
{
OWSAssertIsOnMainThread();
@ -279,7 +235,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
[NSNotificationCenter.defaultCenter postNotificationName:OWSWindowManagerCallDidChangeNotification object:nil];
}
- (void)startCall:(UIViewController *)callViewController
- (void)startCall:(UIViewController<CallViewControllerWindowReference> *)callViewController
{
OWSAssertIsOnMainThread();
OWSAssertDebug(callViewController);
@ -298,7 +254,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
[self ensureWindowState];
}
- (void)endCall:(UIViewController *)callViewController
- (void)endCall:(UIViewController<CallViewControllerWindowReference> *)callViewController
{
OWSAssertIsOnMainThread();
OWSAssertDebug(callViewController);
@ -337,6 +293,7 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
self.shouldShowCallView = YES;
[self.callViewController returnFromPipWithPipWindow:self.returnToCallWindow];
[self ensureWindowState];
}
@ -373,14 +330,13 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
// Show Call View.
[self ensureRootWindowHidden];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowShown];
[self ensureReturnToCallWindowHidden];
[self ensureScreenBlockWindowHidden];
} else {
// Show Root Window
[self ensureRootWindowShown];
[self ensureCallViewWindowHidden];
[self ensureScreenBlockWindowHidden];
if (self.callViewController) {
@ -390,6 +346,8 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
} else {
[self ensureReturnToCallWindowHidden];
}
[self ensureCallViewWindowHidden];
}
}
@ -430,10 +388,9 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
return;
}
[self ensureReturnToCallWindowFrame];
OWSLogInfo(@"showing 'return to call' window.");
self.returnToCallWindow.hidden = NO;
[self.returnToCallViewController startAnimating];
[self.returnToCallViewController displayForCallViewController:self.callViewController];
}
- (void)ensureReturnToCallWindowHidden
@ -446,7 +403,6 @@ const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
OWSLogInfo(@"hiding 'return to call' window.");
self.returnToCallWindow.hidden = YES;
[self.returnToCallViewController stopAnimating];
}
- (void)ensureCallViewWindowShown