session-ios/SignalMessaging/utils/OWSWindowManager.m

610 lines
17 KiB
Mathematica
Raw Normal View History

//
2019-01-08 17:47:40 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSWindowManager.h"
2018-10-12 22:39:40 +02:00
#import "Environment.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalMessaging/SignalMessaging-Swift.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSWindowManagerCallDidChangeNotification = @"OWSWindowManagerCallDidChangeNotification";
2018-07-17 21:40:54 +02:00
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;
}
2018-04-18 18:48:31 +02:00
// Behind everything, especially the root window.
const UIWindowLevel UIWindowLevel_Background = -1.f;
2018-04-18 18:48:31 +02:00
const UIWindowLevel UIWindowLevel_ReturnToCall(void);
const UIWindowLevel UIWindowLevel_ReturnToCall(void)
{
return UIWindowLevelStatusBar - 1;
}
2018-05-16 16:14:11 +02:00
2018-04-18 18:48:31 +02:00
// In front of the root window, behind the screen blocking window.
const UIWindowLevel UIWindowLevel_CallView(void);
const UIWindowLevel UIWindowLevel_CallView(void)
{
2018-04-18 18:48:31 +02:00
return UIWindowLevelNormal + 1.f;
}
2018-05-16 16:14:11 +02:00
// In front of the status bar and CallView
const UIWindowLevel UIWindowLevel_ScreenBlocking(void);
const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
{
2018-04-18 18:48:31 +02:00
return UIWindowLevelStatusBar + 2.f;
}
// In front of everything
2018-07-10 23:34:22 +02:00
const UIWindowLevel UIWindowLevel_MessageActions(void);
const UIWindowLevel UIWindowLevel_MessageActions(void)
{
// Note: To cover the keyboard, this is higher than the ScreenBlocking level,
// but this window is hidden when screen protection is shown.
return CGFLOAT_MAX - 100;
2018-07-10 23:34:22 +02:00
}
2019-01-08 17:47:40 +01:00
#pragma mark -
2018-07-12 16:58:20 +02:00
@interface MessageActionsWindow : UIWindow
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
2018-07-12 16:58:20 +02:00
@implementation MessageActionsWindow
- (UIWindowLevel)windowLevel
{
// As of iOS11, setWindowLevel clamps the value below
// the height of the keyboard window.
// Because we want to display above the keyboard, we hardcode
// the `windowLevel` getter.
return UIWindowLevel_MessageActions();
}
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@implementation OWSWindowRootViewController
- (BOOL)canBecomeFirstResponder
{
return YES;
}
2018-10-25 19:02:30 +02:00
#pragma mark - Orientation
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@interface OWSWindowRootNavigationViewController : UINavigationController
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@implementation OWSWindowRootNavigationViewController : UINavigationController
#pragma mark - Orientation
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
@end
#pragma mark -
@interface OWSWindowManager () <ReturnToCallViewControllerDelegate>
// UIWindowLevelNormal
@property (nonatomic) UIWindow *rootWindow;
// UIWindowLevel_ReturnToCall
@property (nonatomic) UIWindow *returnToCallWindow;
@property (nonatomic) ReturnToCallViewController *returnToCallViewController;
// UIWindowLevel_CallView
@property (nonatomic) UIWindow *callViewWindow;
@property (nonatomic) UINavigationController *callNavigationController;
2018-07-10 23:34:22 +02:00
// UIWindowLevel_MessageActions
@property (nonatomic) UIWindow *menuActionsWindow;
@property (nonatomic, nullable) UIViewController *menuActionsViewController;
2018-07-10 23:34:22 +02:00
// UIWindowLevel_Background if inactive,
// UIWindowLevel_ScreenBlocking() if active.
@property (nonatomic) UIWindow *screenBlockingWindow;
@property (nonatomic) BOOL isScreenBlockActive;
2018-05-16 16:23:23 +02:00
@property (nonatomic) BOOL shouldShowCallView;
@property (nonatomic, nullable) UIViewController *callViewController;
@end
#pragma mark -
@implementation OWSWindowManager
+ (instancetype)sharedManager
{
2018-10-12 22:39:40 +02:00
OWSAssertDebug(Environment.shared.windowManager);
return Environment.shared.windowManager;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
OWSAssertIsOnMainThread();
OWSSingletonAssert();
return self;
}
- (void)setupWithRootWindow:(UIWindow *)rootWindow screenBlockingWindow:(UIWindow *)screenBlockingWindow
{
OWSAssertIsOnMainThread();
OWSAssertDebug(rootWindow);
OWSAssertDebug(!self.rootWindow);
OWSAssertDebug(screenBlockingWindow);
OWSAssertDebug(!self.screenBlockingWindow);
self.rootWindow = rootWindow;
self.screenBlockingWindow = screenBlockingWindow;
2018-04-18 18:48:31 +02:00
self.returnToCallWindow = [self createReturnToCallWindow:rootWindow];
self.callViewWindow = [self createCallViewWindow:rootWindow];
self.menuActionsWindow = [self createMenuActionsWindowWithRoowWindow:rootWindow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangeStatusBarFrame:)
name:UIApplicationDidChangeStatusBarFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
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 = self.returnToCallWindow.frame;
2018-07-17 21:40:54 +02:00
newFrame.size.height = OWSWindowManagerCallBannerHeight();
OWSLogDebug(@"returnToCallWindowFrame: %@", NSStringFromCGRect(newFrame));
self.returnToCallWindow.frame = newFrame;
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
[self hideMenuActionsWindow];
}
2018-04-18 18:48:31 +02:00
- (UIWindow *)createReturnToCallWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssertDebug(rootWindow);
// "Return to call" should remain at the top of the screen.
CGRect windowFrame = UIScreen.mainScreen.bounds;
2018-07-17 21:40:54 +02:00
windowFrame.size.height = OWSWindowManagerCallBannerHeight();
UIWindow *window = [[UIWindow alloc] initWithFrame:windowFrame];
window.hidden = YES;
window.windowLevel = UIWindowLevel_ReturnToCall();
window.opaque = YES;
ReturnToCallViewController *viewController = [ReturnToCallViewController new];
self.returnToCallViewController = viewController;
viewController.delegate = self;
window.rootViewController = viewController;
return window;
}
- (UIWindow *)createMenuActionsWindowWithRoowWindow:(UIWindow *)rootWindow
2018-07-10 23:34:22 +02:00
{
2018-07-12 16:58:20 +02:00
UIWindow *window;
if (@available(iOS 11, *)) {
// On iOS11, setting the windowLevel is insufficient, so we override
// the `windowLevel` getter.
window = [[MessageActionsWindow alloc] initWithFrame:rootWindow.bounds];
} else {
// On iOS9, 10 overriding the `windowLevel` getter does not cause the
// window to be displayed above the keyboard, but setting the window
// level works.
window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
window.windowLevel = UIWindowLevel_MessageActions();
}
2018-07-10 23:34:22 +02:00
window.hidden = YES;
window.backgroundColor = UIColor.clearColor;
return window;
}
2018-04-18 18:48:31 +02:00
- (UIWindow *)createCallViewWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssertDebug(rootWindow);
UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
window.hidden = YES;
window.windowLevel = UIWindowLevel_CallView();
window.opaque = YES;
2018-04-18 18:48:31 +02:00
// TODO: What's the right color to use here?
2018-04-18 18:09:42 +02:00
window.backgroundColor = [UIColor ows_materialBlueColor];
UIViewController *viewController = [OWSWindowRootViewController new];
2018-04-18 18:09:42 +02:00
viewController.view.backgroundColor = [UIColor ows_materialBlueColor];
// NOTE: Do not use OWSNavigationController for call window.
2018-05-30 18:35:55 +02:00
// It adjusts the size of the navigation bar to reflect the
// call window. We don't want those adjustments made within
// the call window itself.
OWSWindowRootNavigationViewController *navigationController =
[[OWSWindowRootNavigationViewController alloc] initWithRootViewController:viewController];
navigationController.navigationBarHidden = YES;
OWSAssertDebug(!self.callNavigationController);
2018-04-18 18:48:31 +02:00
self.callNavigationController = navigationController;
window.rootViewController = navigationController;
return window;
}
- (void)setIsScreenBlockActive:(BOOL)isScreenBlockActive
{
OWSAssertIsOnMainThread();
_isScreenBlockActive = isScreenBlockActive;
[self ensureWindowState];
}
#pragma mark - Message Actions
- (BOOL)isPresentingMenuActions
{
return self.menuActionsViewController != nil;
}
- (void)showMenuActionsWindow:(UIViewController *)menuActionsViewController
2018-07-10 23:34:22 +02:00
{
OWSAssertDebug(self.menuActionsViewController == nil);
self.menuActionsViewController = menuActionsViewController;
self.menuActionsWindow.rootViewController = menuActionsViewController;
2018-07-10 23:34:22 +02:00
[self ensureWindowState];
}
- (void)hideMenuActionsWindow
2018-07-10 23:34:22 +02:00
{
self.menuActionsWindow.rootViewController = nil;
self.menuActionsViewController = nil;
2018-07-10 23:34:22 +02:00
[self ensureWindowState];
}
#pragma mark - Calls
- (void)setCallViewController:(nullable UIViewController *)callViewController
{
OWSAssertIsOnMainThread();
if (callViewController == _callViewController) {
return;
}
_callViewController = callViewController;
[NSNotificationCenter.defaultCenter postNotificationName:OWSWindowManagerCallDidChangeNotification object:nil];
}
- (void)startCall:(UIViewController *)callViewController
{
OWSAssertIsOnMainThread();
OWSAssertDebug(callViewController);
OWSAssertDebug(!self.callViewController);
self.callViewController = callViewController;
2018-04-18 19:33:29 +02:00
// Attach callViewController to window.
[self.callNavigationController popToRootViewControllerAnimated:NO];
2018-04-18 18:48:31 +02:00
[self.callNavigationController pushViewController:callViewController animated:NO];
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = YES;
[self ensureWindowState];
}
- (void)endCall:(UIViewController *)callViewController
{
OWSAssertIsOnMainThread();
OWSAssertDebug(callViewController);
OWSAssertDebug(self.callViewController);
if (self.callViewController != callViewController) {
OWSLogWarn(@"Ignoring end call request from obsolete call view controller.");
return;
}
// Dettach callViewController from window.
2018-04-18 18:48:31 +02:00
[self.callNavigationController popToRootViewControllerAnimated:NO];
self.callViewController = nil;
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = NO;
[self ensureWindowState];
}
- (void)leaveCallView
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.callViewController);
OWSAssertDebug(self.shouldShowCallView);
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = NO;
[self ensureWindowState];
}
- (void)showCallView
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.callViewController);
OWSAssertDebug(!self.shouldShowCallView);
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = YES;
[self ensureWindowState];
}
2018-04-18 18:09:42 +02:00
- (BOOL)hasCall
{
OWSAssertIsOnMainThread();
return self.callViewController != nil;
}
#pragma mark - Window State
- (void)ensureWindowState
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.rootWindow);
OWSAssertDebug(self.returnToCallWindow);
OWSAssertDebug(self.callViewWindow);
OWSAssertDebug(self.screenBlockingWindow);
2018-04-24 23:00:39 +02:00
// To avoid bad frames, we never want to hide the blocking window, so we manipulate
// its window level to "hide" it behind other windows. The other windows have fixed
// window level and are shown/hidden as necessary.
//
// Note that we always "hide" before we "show".
if (self.isScreenBlockActive) {
// Show Screen Block.
2018-04-24 23:00:39 +02:00
[self ensureRootWindowHidden];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowHidden];
2018-07-10 23:34:22 +02:00
[self ensureMessageActionsWindowHidden];
2018-04-24 23:00:39 +02:00
[self ensureScreenBlockWindowShown];
2018-05-16 16:23:23 +02:00
} else if (self.callViewController && self.shouldShowCallView) {
// Show Call View.
2018-04-24 23:00:39 +02:00
[self ensureRootWindowHidden];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowShown];
2018-07-10 23:34:22 +02:00
[self ensureMessageActionsWindowHidden];
2018-04-24 23:00:39 +02:00
[self ensureScreenBlockWindowHidden];
2018-08-22 21:10:30 +02:00
} else {
// Show Root Window
2018-05-17 22:26:17 +02:00
[self ensureRootWindowShown];
2018-04-24 23:00:39 +02:00
[self ensureCallViewWindowHidden];
2018-07-10 23:34:22 +02:00
[self ensureScreenBlockWindowHidden];
2018-08-22 21:10:30 +02:00
if (self.callViewController) {
// Add "Return to Call" banner
[self ensureReturnToCallWindowShown];
} else {
2018-08-22 21:10:30 +02:00
[self ensureReturnToCallWindowHidden];
}
2018-07-10 23:34:22 +02:00
2018-08-22 21:10:30 +02:00
if (self.menuActionsViewController) {
// Add "Message Actions" action sheet
2018-08-22 21:10:30 +02:00
[self ensureMessageActionsWindowShown];
2018-08-22 21:10:30 +02:00
// Don't hide rootWindow so as not to dismiss keyboard.
OWSAssertDebug(!self.rootWindow.isHidden);
2018-08-22 21:10:30 +02:00
} else {
[self ensureMessageActionsWindowHidden];
}
}
}
2018-05-17 22:26:17 +02:00
- (void)ensureRootWindowShown
{
OWSAssertIsOnMainThread();
if (self.rootWindow.hidden) {
OWSLogInfo(@"showing root window.");
}
// By calling makeKeyAndVisible we ensure the rootViewController becomes firt responder.
// In the normal case, that means the SignalViewController will call `becomeFirstResponder`
// on the vc on top of its navigation stack.
[self.rootWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureRootWindowHidden
{
OWSAssertIsOnMainThread();
if (!self.rootWindow.hidden) {
OWSLogInfo(@"hiding root window.");
}
self.rootWindow.hidden = YES;
}
2018-04-24 23:00:39 +02:00
- (void)ensureReturnToCallWindowShown
{
OWSAssertIsOnMainThread();
if (!self.returnToCallWindow.hidden) {
return;
}
[self ensureReturnToCallWindowFrame];
OWSLogInfo(@"showing 'return to call' window.");
self.returnToCallWindow.hidden = NO;
[self.returnToCallViewController startAnimating];
}
2018-04-24 23:00:39 +02:00
- (void)ensureReturnToCallWindowHidden
{
OWSAssertIsOnMainThread();
if (self.returnToCallWindow.hidden) {
return;
}
OWSLogInfo(@"hiding 'return to call' window.");
self.returnToCallWindow.hidden = YES;
[self.returnToCallViewController stopAnimating];
}
2018-04-24 23:00:39 +02:00
- (void)ensureCallViewWindowShown
{
OWSAssertIsOnMainThread();
if (self.callViewWindow.hidden) {
OWSLogInfo(@"showing call window.");
}
[self.callViewWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureCallViewWindowHidden
{
OWSAssertIsOnMainThread();
if (!self.callViewWindow.hidden) {
OWSLogInfo(@"hiding call window.");
}
self.callViewWindow.hidden = YES;
}
2018-07-10 23:34:22 +02:00
- (void)ensureMessageActionsWindowShown
{
OWSAssertIsOnMainThread();
if (self.menuActionsWindow.hidden) {
OWSLogInfo(@"showing message actions window.");
2018-07-10 23:34:22 +02:00
}
// Do not make key, we want the keyboard to stay popped.
self.menuActionsWindow.hidden = NO;
2018-07-10 23:34:22 +02:00
}
- (void)ensureMessageActionsWindowHidden
{
OWSAssertIsOnMainThread();
if (!self.menuActionsWindow.hidden) {
OWSLogInfo(@"hiding message actions window.");
2018-07-10 23:34:22 +02:00
}
self.menuActionsWindow.hidden = YES;
2018-07-10 23:34:22 +02:00
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowShown
{
OWSAssertIsOnMainThread();
if (self.screenBlockingWindow.windowLevel != UIWindowLevel_ScreenBlocking()) {
OWSLogInfo(@"showing block window.");
}
self.screenBlockingWindow.windowLevel = UIWindowLevel_ScreenBlocking();
[self.screenBlockingWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowHidden
{
OWSAssertIsOnMainThread();
if (self.screenBlockingWindow.windowLevel != UIWindowLevel_Background) {
OWSLogInfo(@"hiding block window.");
}
// Never hide the blocking window (that can lead to bad frames).
// Instead, manipulate its window level to move it in front of
// or behind the root window.
self.screenBlockingWindow.windowLevel = UIWindowLevel_Background;
}
#pragma mark - ReturnToCallViewControllerDelegate
- (void)returnToCallWasTapped:(ReturnToCallViewController *)viewController
{
[self showCallView];
}
@end
NS_ASSUME_NONNULL_END