// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSWindowManager.h" #import "Signal-Swift.h" #import #import #import NS_ASSUME_NONNULL_BEGIN // Behind everything, especially the root window. const UIWindowLevel UIWindowLevel_Background = -1.f; // In front of the root window _and_ status bar // but behind the screen blocking window. const UIWindowLevel UIWindowLevel_ReturnToCall(void); const UIWindowLevel UIWindowLevel_ReturnToCall(void) { return UIWindowLevelStatusBar + 1.f; } // In front of the root window, behind the screen blocking window. const UIWindowLevel UIWindowLevel_CallView(void); const UIWindowLevel UIWindowLevel_CallView(void) { return UIWindowLevelNormal + 1.f; } // In front of everything, including the status bar. const UIWindowLevel UIWindowLevel_ScreenBlocking(void); const UIWindowLevel UIWindowLevel_ScreenBlocking(void) { return UIWindowLevelStatusBar + 2.f; } // TODO: Verify that this is the correct height for: // CallKit, non-CallKit, incoming and outgoing calls. const int kReturnToCallWindowHeight = 20.f; @implementation OWSWindowRootViewController - (BOOL)canBecomeFirstResponder { return YES; } @end #pragma mark - @interface OWSWindowManager () // UIWindowLevelNormal @property (nonatomic) UIWindow *rootWindow; // UIWindowLevel_ReturnToCall @property (nonatomic) UIWindow *returnToCallWindow; @property (nonatomic) UILabel *returnToCallLabel; // UIWindowLevel_CallView @property (nonatomic) UIWindow *callViewWindow; @property (nonatomic) UINavigationController *callNavigationController; // UIWindowLevel_Background if inactive, // UIWindowLevel_ScreenBlocking() if active. @property (nonatomic) UIWindow *screenBlockingWindow; @property (nonatomic) BOOL isScreenBlockActive; @property (nonatomic) BOOL isCallViewActive; @property (nonatomic, nullable) UIViewController *callViewController; @property (nonatomic, nullable) UIResponder *rootWindowResponder; @property (nonatomic, nullable, weak) UIViewController *rootFrontmostViewController; @end #pragma mark - @implementation OWSWindowManager + (instancetype)sharedManager { static OWSWindowManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] initDefault]; }); return instance; } - (instancetype)initDefault { self = [super init]; if (!self) { return self; } OWSAssertIsOnMainThread(); OWSSingletonAssert(); return self; } - (void)setupWithRootWindow:(UIWindow *)rootWindow screenBlockingWindow:(UIWindow *)screenBlockingWindow { OWSAssertIsOnMainThread(); OWSAssert(rootWindow); OWSAssert(!self.rootWindow); OWSAssert(screenBlockingWindow); OWSAssert(!self.screenBlockingWindow); self.rootWindow = rootWindow; self.screenBlockingWindow = screenBlockingWindow; self.returnToCallWindow = [self createReturnToCallWindow:rootWindow]; self.callViewWindow = [self createCallViewWindow:rootWindow]; [self ensureWindowState]; } - (UIWindow *)createReturnToCallWindow:(UIWindow *)rootWindow { OWSAssertIsOnMainThread(); OWSAssert(rootWindow); // "Return to call" should remain at the top of the screen. // // TODO: Extend below the status bar. CGRect windowFrame = rootWindow.bounds; windowFrame.size.height = kReturnToCallWindowHeight; UIWindow *window = [[UIWindow alloc] initWithFrame:windowFrame]; window.hidden = YES; window.windowLevel = UIWindowLevel_ReturnToCall(); window.opaque = YES; // This is the color of the iOS "return to call" banner. // TODO: What's the right color to use here? UIColor *backgroundColor = [UIColor colorWithRGBHex:0x4cd964]; window.backgroundColor = backgroundColor; UIViewController *viewController = [OWSWindowRootViewController new]; viewController.view.backgroundColor = backgroundColor; UIView *rootView = viewController.view; rootView.userInteractionEnabled = YES; [rootView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(returnToCallWasTapped:)]]; UILabel *label = [UILabel new]; label.text = NSLocalizedString(@"CALL_WINDOW_RETURN_TO_CALL", @"Label for the 'return to call' banner."); label.textColor = [UIColor whiteColor]; // TODO: Dynamic type? label.font = [UIFont ows_regularFontWithSize:14.f]; [rootView addSubview:label]; [label autoCenterInSuperview]; self.returnToCallLabel = label; window.rootViewController = viewController; return window; } - (UIWindow *)createCallViewWindow:(UIWindow *)rootWindow { OWSAssertIsOnMainThread(); OWSAssert(rootWindow); UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds]; window.hidden = YES; window.windowLevel = UIWindowLevel_CallView(); window.opaque = YES; // TODO: What's the right color to use here? window.backgroundColor = [UIColor ows_materialBlueColor]; UIViewController *viewController = [OWSWindowRootViewController new]; viewController.view.backgroundColor = [UIColor ows_materialBlueColor]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; navigationController.navigationBarHidden = YES; self.callNavigationController = navigationController; window.rootViewController = navigationController; return window; } - (void)setIsScreenBlockActive:(BOOL)isScreenBlockActive { OWSAssertIsOnMainThread(); _isScreenBlockActive = isScreenBlockActive; [self ensureWindowState]; } #pragma mark - Calls - (void)startCall:(UIViewController *)callViewController { OWSAssertIsOnMainThread(); OWSAssert(callViewController); OWSAssert(!self.callViewController); self.callViewController = callViewController; // Attach callViewController to window. [self.callNavigationController popToRootViewControllerAnimated:NO]; [self.callNavigationController pushViewController:callViewController animated:NO]; self.isCallViewActive = YES; [self ensureWindowState]; } - (void)endCall:(UIViewController *)callViewController { OWSAssertIsOnMainThread(); OWSAssert(callViewController); OWSAssert(self.callViewController); if (self.callViewController != callViewController) { DDLogWarn(@"%@ Ignoring end call request from obsolete call view controller.", self.logTag); return; } // Dettach callViewController from window. [self.callNavigationController popToRootViewControllerAnimated:NO]; self.callViewController = nil; self.isCallViewActive = NO; [self ensureWindowState]; } - (void)leaveCallView { OWSAssertIsOnMainThread(); OWSAssert(self.callViewController); OWSAssert(self.isCallViewActive); self.isCallViewActive = NO; [self ensureWindowState]; } - (void)returnToCallView { OWSAssertIsOnMainThread(); OWSAssert(self.callViewController); OWSAssert(!self.isCallViewActive); self.isCallViewActive = YES; [self ensureWindowState]; } - (BOOL)hasCall { OWSAssertIsOnMainThread(); return self.callViewController != nil; } #pragma mark - Window State - (void)ensureWindowState { OWSAssertIsOnMainThread(); OWSAssert(self.rootWindow); OWSAssert(self.returnToCallWindow); OWSAssert(self.callViewWindow); OWSAssert(self.screenBlockingWindow); if (self.isScreenBlockActive) { // Show Screen Block. [self hideRootWindowIfNecessary]; [self hideReturnToCallWindowIfNecessary]; [self hideCallViewWindowIfNecessary]; [self showScreenBlockWindowIfNecessary]; } else if (self.callViewController && self.isCallViewActive) { // Show Call View. [self hideRootWindowIfNecessary]; [self hideReturnToCallWindowIfNecessary]; [self showCallViewWindowIfNecessary]; [self hideScreenBlockWindowIfNecessary]; } else if (self.callViewController) { // Show Root Window + "Return to Call". [self showRootWindowIfNecessary]; [self showReturnToCallWindowIfNecessary]; [self hideCallViewWindowIfNecessary]; [self hideScreenBlockWindowIfNecessary]; } else { // Show Root Window [self showRootWindowIfNecessary]; [self hideReturnToCallWindowIfNecessary]; [self hideCallViewWindowIfNecessary]; [self hideScreenBlockWindowIfNecessary]; } DDLogVerbose(@"%@ rootWindow: %d %f", self.logTag, self.rootWindow.hidden, self.rootWindow.windowLevel); DDLogVerbose(@"%@ returnToCallWindow: %d %f", self.logTag, self.returnToCallWindow.hidden, self.returnToCallWindow.windowLevel); DDLogVerbose(@"%@ callViewWindow: %d %f", self.logTag, self.callViewWindow.hidden, self.callViewWindow.windowLevel); DDLogVerbose(@"%@ screenBlockingWindow: %d %f", self.logTag, self.screenBlockingWindow.hidden, self.screenBlockingWindow.windowLevel); dispatch_async(dispatch_get_main_queue(), ^{ DDLogVerbose(@"%@ ...rootWindow: %d %f", self.logTag, self.rootWindow.hidden, self.rootWindow.windowLevel); DDLogVerbose(@"%@ ...returnToCallWindow: %d %f", self.logTag, self.returnToCallWindow.hidden, self.returnToCallWindow.windowLevel); DDLogVerbose( @"%@ ...callViewWindow: %d %f", self.logTag, self.callViewWindow.hidden, self.callViewWindow.windowLevel); DDLogVerbose(@"%@ ...screenBlockingWindow: %d %f", self.logTag, self.screenBlockingWindow.hidden, self.screenBlockingWindow.windowLevel); }); } - (void)showRootWindowIfNecessary { OWSAssertIsOnMainThread(); if (self.rootWindow.hidden) { DDLogInfo(@"%@ showing root window.", self.logTag); } BOOL shouldTryToRestoreFirstResponder = self.rootWindow.hidden; [self.rootWindow makeKeyAndVisible]; // When we hide the block window, try to restore the first // responder of the root window. // // It's important we restore first responder status once the user completes // In some cases, (RegistrationLock Reminder) it just puts the keyboard back where // the user needs it, saving them a tap. // But in the case of an inputAccessoryView, like the ConversationViewController, // failing to restore firstResponder could hide the input toolbar. if (shouldTryToRestoreFirstResponder) { UIViewController *rootFrontmostViewController = [UIApplication.sharedApplication frontmostViewControllerIgnoringAlerts]; DDLogInfo(@"%@ trying to restore self.rootWindowResponder: %@ (%@ ? %@ == %d)", self.logTag, self.rootWindowResponder, [self.rootFrontmostViewController class], rootFrontmostViewController, self.rootFrontmostViewController == rootFrontmostViewController); if (self.rootFrontmostViewController == rootFrontmostViewController) { [self.rootWindowResponder becomeFirstResponder]; } else { [rootFrontmostViewController becomeFirstResponder]; } } self.rootWindowResponder = nil; self.rootFrontmostViewController = nil; } - (void)hideRootWindowIfNecessary { OWSAssertIsOnMainThread(); if (!self.rootWindow.hidden) { DDLogInfo(@"%@ hiding root window.", self.logTag); } // When we hide the root window, try to capture its first responder and // current vc before it is hidden. if (!self.rootWindow.hidden) { self.rootWindowResponder = [UIResponder currentFirstResponder]; self.rootFrontmostViewController = [UIApplication.sharedApplication frontmostViewControllerIgnoringAlerts]; DDLogInfo(@"%@ trying to capture self.rootWindowResponder: %@ (%@)", self.logTag, self.rootWindowResponder, [self.rootFrontmostViewController class]); } self.rootWindow.hidden = YES; } - (void)showReturnToCallWindowIfNecessary { OWSAssertIsOnMainThread(); if (self.returnToCallWindow.hidden) { DDLogInfo(@"%@ showing 'return to call' window.", self.logTag); [self.returnToCallLabel.layer removeAllAnimations]; self.returnToCallLabel.alpha = 1.f; [UIView animateWithDuration:1.f delay:0.f options:(UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionBeginFromCurrentState) animations:^{ self.returnToCallLabel.alpha = 0.f; } completion:^(BOOL finished) { self.returnToCallLabel.alpha = 1.f; }]; } self.returnToCallWindow.hidden = NO; } - (void)hideReturnToCallWindowIfNecessary { OWSAssertIsOnMainThread(); if (!self.returnToCallWindow.hidden) { DDLogInfo(@"%@ hiding 'return to call' window.", self.logTag); } self.returnToCallWindow.hidden = YES; [self.returnToCallLabel.layer removeAllAnimations]; } - (void)showCallViewWindowIfNecessary { OWSAssertIsOnMainThread(); if (self.callViewWindow.hidden) { DDLogInfo(@"%@ showing call window.", self.logTag); } [self.callViewWindow makeKeyAndVisible]; [self.callViewWindow.rootViewController becomeFirstResponder]; } - (void)hideCallViewWindowIfNecessary { OWSAssertIsOnMainThread(); if (!self.callViewWindow.hidden) { DDLogInfo(@"%@ hiding call window.", self.logTag); } self.callViewWindow.hidden = YES; } - (void)showScreenBlockWindowIfNecessary { OWSAssertIsOnMainThread(); if (self.screenBlockingWindow.windowLevel != UIWindowLevel_ScreenBlocking()) { DDLogInfo(@"%@ showing block window.", self.logTag); } self.screenBlockingWindow.windowLevel = UIWindowLevel_ScreenBlocking(); [self.screenBlockingWindow.rootViewController becomeFirstResponder]; } - (void)hideScreenBlockWindowIfNecessary { OWSAssertIsOnMainThread(); if (self.screenBlockingWindow.windowLevel != UIWindowLevel_Background) { DDLogInfo(@"%@ hiding block window.", self.logTag); } // 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; [self.screenBlockingWindow resignFirstResponder]; } #pragma mark - Events - (void)returnToCallWasTapped:(UIGestureRecognizer *)sender { if (sender.state != UIGestureRecognizerStateRecognized) { return; } [self returnToCallView]; } @end NS_ASSUME_NONNULL_END