// // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // #import "WindowManagerFix.h" #import void fixit_workAroundRotationIssue(UIWindow *window) { // ### 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) { OWSCFailDebug(@"selectorString1 was unexpectedly nil"); return; } SEL selector1 = NSSelectorFromString(selectorString1); if (![window respondsToSelector:selector1]) { OWSCFailDebug(@"failure: doesn't respond to selector1"); return; } IMP imp1 = [window methodForSelector:selector1]; BOOL (*func1)(id, SEL) = (void *)imp1; BOOL isDisabled = func1(window, selector1); if (isDisabled) { OWSLogInfo(@"autorotation is disabled."); // 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) { OWSCFailDebug(@"klassString was unexpectedly nil"); return; } id klass = NSClassFromString(klassString); if (klass == nil) { OWSCFailDebug(@"klass was unexpectedly nil"); return; } // NSString *encodedSelector2String = @"supportForScreen:".encodedForSelector; NSString *encodedSelector2String = @"BQcCAgEEBlcBBGR0BHZ2AEs="; NSString *_Nullable selector2String = encodedSelector2String.decodedForSelector; if (selector2String == nil) { OWSCFailDebug(@"selector2String was unexpectedly nil"); return; } SEL selector2 = NSSelectorFromString(selector2String); if (![klass respondsToSelector:selector2]) { OWSCFailDebug(@"klass didn't respond to selector"); 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) { OWSCFailDebug(@"selector3String was unexpectedly nil"); return; } SEL selector3 = NSSelectorFromString(selector3String); if (![dismissSupport respondsToSelector:selector3]) { OWSCFailDebug(@"dismissSupport didn't respond to selector"); return; } IMP imp3 = [dismissSupport methodForSelector:selector3]; void (*func3)(id, SEL) = (void *)imp3; func3(dismissSupport, selector3); OWSLogInfo(@"finished scrollView transition"); } }