session-ios/SessionMessagingKit/Utilities/OWSWindowManager.m

296 lines
9.4 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"
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
2020-11-26 00:37:56 +01:00
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const IsScreenBlockActiveDidChangeNotification = @"IsScreenBlockActiveDidChangeNotification";
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
// 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;
}
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 UIInterfaceOrientationMaskAllButUpsideDown;
2018-10-25 19:02:30 +02:00
}
@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 UIInterfaceOrientationMaskAllButUpsideDown;
}
@end
#pragma mark -
2020-11-16 00:34:47 +01:00
@interface OWSWindowManager ()
// UIWindowLevelNormal
@property (nonatomic) UIWindow *rootWindow;
// UIWindowLevel_Background if inactive,
// UIWindowLevel_ScreenBlocking() if active.
@property (nonatomic) UIWindow *screenBlockingWindow;
@end
#pragma mark -
@implementation OWSWindowManager
+ (instancetype)sharedManager
{
return SMKEnvironment.shared.windowManager;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
return self;
}
- (void)setupWithRootWindow:(UIWindow *)rootWindow screenBlockingWindow:(UIWindow *)screenBlockingWindow
{
self.rootWindow = rootWindow;
self.screenBlockingWindow = screenBlockingWindow;
[[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
{
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
}
- (void)setIsScreenBlockActive:(BOOL)isScreenBlockActive
{
_isScreenBlockActive = isScreenBlockActive;
[self ensureWindowState];
[[NSNotificationCenter defaultCenter] postNotificationName:IsScreenBlockActiveDidChangeNotification
object:nil
userInfo:nil];
}
2019-01-09 20:32:52 +01:00
- (BOOL)isAppWindow:(UIWindow *)window
{
return (window == self.rootWindow || window == self.screenBlockingWindow);
2018-04-18 18:09:42 +02:00
}
#pragma mark - Window State
- (void)ensureWindowState
{
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 ensureScreenBlockWindowShown];
}
else {
2018-08-22 21:10:30 +02:00
// Show Root Window
2018-05-17 22:26:17 +02:00
[self ensureRootWindowShown];
2018-07-10 23:34:22 +02:00
[self ensureScreenBlockWindowHidden];
}
}
2018-05-17 22:26:17 +02:00
- (void)ensureRootWindowShown
{
2019-01-10 15:37:33 +01:00
// By calling makeKeyAndVisible we ensure the rootViewController becomes first responder.
// In the normal case, that means the SignalViewController will call `becomeFirstResponder`
// on the vc on top of its navigation stack.
[self.rootWindow makeKeyAndVisible];
2019-03-18 19:08:40 +01:00
[self fixit_workAroundRotationIssue];
}
2018-04-24 23:00:39 +02:00
- (void)ensureRootWindowHidden
{
self.rootWindow.hidden = YES;
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowShown
{
self.screenBlockingWindow.windowLevel = UIWindowLevel_ScreenBlocking();
[self.screenBlockingWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowHidden
{
// 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;
}
2019-03-18 19:08:40 +01:00
#pragma mark - Fixit
- (void)fixit_workAroundRotationIssue
{
// ### Symptom
//
// The app can get into a degraded state where the main window will incorrectly remain locked in
// portrait mode. Worse yet, the status bar and input window will continue to rotate with respect
// to the device orientation. So once you're in this degraded state, the status bar and input
// window can be in landscape while simultaneoulsy the view controller behind them is in portrait.
//
// ### To Reproduce
//
// On an iPhone6 (not reproducible on an iPhoneX)
//
// 0. Ensure "screen protection" is enabled (not necessarily screen lock)
// 1. Enter Conversation View Controller
// 2. Pop Keyboard
// 3. Begin dismissing keyboard with one finger, but stopping when it's about 50% dismissed,
// keep your finger there with the keyboard partially dismissed.
// 4. With your other hand, hit the home button to leave Signal.
// 5. Re-enter Signal
// 6. Rotate to landscape
//
// Expected: Conversation View, Input Toolbar window, and Settings Bar should all rotate to landscape.
// Actual: The input toolbar and the settings toolbar rotate to landscape, but the Conversation
// View remains in portrait, this looks super broken.
//
// ### Background
//
// Some debugging shows that the `ConversationViewController.view.window.isInterfaceAutorotationDisabled`
// is true. This is a private property, whose function we don't exactly know, but it seems like
// `interfaceAutorotation` is disabled when certain transition animations begin, and then
// re-enabled once the animation completes.
//
// My best guess is that autorotation is intended to be disabled for the duration of the
// interactive-keyboard-dismiss-transition, so when we start the interactive dismiss, autorotation
// has been disabled, but because we hide the main app window in the middle of the transition,
// autorotation doesn't have a chance to be re-enabled.
//
// ## So, The Fix
//
// If we find ourself in a situation where autorotation is disabled while showing the rootWindow,
// we re-enable autorotation.
// NSString *encodedSelectorString1 = @"isInterfaceAutorotationDisabled".encodedForSelector;
NSString *encodedSelectorString1 = @"egVaAAZ2BHdydHZSBwYBBAEGcgZ6AQBVegVyc312dQ==";
NSString *_Nullable selectorString1 = encodedSelectorString1.decodedForSelector;
if (selectorString1 == nil) {
return;
}
SEL selector1 = NSSelectorFromString(selectorString1);
if (![self.rootWindow respondsToSelector:selector1]) {
return;
}
IMP imp1 = [self.rootWindow methodForSelector:selector1];
BOOL (*func1)(id, SEL) = (void *)imp1;
BOOL isDisabled = func1(self.rootWindow, selector1);
if (isDisabled) {
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
// The remainder of this method calls:
// [[UIScrollToDismissSupport supportForScreen:UIScreen.main] finishScrollViewTransition]
// after verifying the methods/classes exist.
// NSString *encodedKlassString = @"UIScrollToDismissSupport".encodedForSelector;
NSString *encodedKlassString = @"ZlpkdAQBfX1lAVV6BX56BQVkBwICAQQG";
NSString *_Nullable klassString = encodedKlassString.decodedForSelector;
if (klassString == nil) {
return;
}
id klass = NSClassFromString(klassString);
if (klass == nil) {
2019-03-18 19:08:40 +01:00
return;
}
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
// NSString *encodedSelector2String = @"supportForScreen:".encodedForSelector;
NSString *encodedSelector2String = @"BQcCAgEEBlcBBGR0BHZ2AEs=";
NSString *_Nullable selector2String = encodedSelector2String.decodedForSelector;
if (selector2String == nil) {
return;
}
SEL selector2 = NSSelectorFromString(selector2String);
if (![klass respondsToSelector:selector2]) {
return;
}
IMP imp2 = [klass methodForSelector:selector2];
id (*func2)(id, SEL, UIScreen *) = (void *)imp2;
id dismissSupport = func2(klass, selector2, UIScreen.mainScreen);
// NSString *encodedSelector3String = @"finishScrollViewTransition".encodedForSelector;
NSString *encodedSelector3String = @"d3oAegV5ZHQEAX19Z3p2CWUEcgAFegZ6AQA=";
NSString *_Nullable selector3String = encodedSelector3String.decodedForSelector;
if (selector3String == nil) {
return;
}
SEL selector3 = NSSelectorFromString(selector3String);
if (![dismissSupport respondsToSelector:selector3]) {
2019-03-18 19:08:40 +01:00
return;
}
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
IMP imp3 = [dismissSupport methodForSelector:selector3];
void (*func3)(id, SEL) = (void *)imp3;
func3(dismissSupport, selector3);
2019-03-18 19:08:40 +01:00
}
}
@end
NS_ASSUME_NONNULL_END