From 78ce3583ed6b27308ee110fe90f7b4c922999be1 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 18 Mar 2019 11:08:40 -0700 Subject: [PATCH] fix rotation issue --- Signal/test/util/StringAdditionsTest.swift | 34 ++++++++- SignalMessaging/utils/OWSWindowManager.m | 87 ++++++++++++++++++++++ SignalServiceKit/src/Util/String+SSK.swift | 63 ++++++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) diff --git a/Signal/test/util/StringAdditionsTest.swift b/Signal/test/util/StringAdditionsTest.swift index 005d742c5..3118ee600 100644 --- a/Signal/test/util/StringAdditionsTest.swift +++ b/Signal/test/util/StringAdditionsTest.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import XCTest @@ -16,7 +16,7 @@ class StringAdditionsTest: SignalBaseTest { super.tearDown() } - func testASCII() { + func test_truncated_ASCII() { let originalString = "Hello World" var truncatedString = originalString.truncated(toByteCount: 8) @@ -35,7 +35,7 @@ class StringAdditionsTest: SignalBaseTest { XCTAssertEqual("Hello World", truncatedString) } - func testMultiByte() { + func test_truncated_MultiByte() { let originalString = "πŸ‡¨πŸ‡¦πŸ‡¨πŸ‡¦πŸ‡¨πŸ‡¦πŸ‡¨πŸ‡¦" var truncatedString = originalString.truncated(toByteCount: 0) @@ -63,7 +63,7 @@ class StringAdditionsTest: SignalBaseTest { XCTAssertEqual("πŸ‡¨πŸ‡¦πŸ‡¨πŸ‡¦", truncatedString) } - func testMixed() { + func test_truncated_Mixed() { let originalString = "OhπŸ‡¨πŸ‡¦CanadaπŸ‡¨πŸ‡¦" var truncatedString = originalString.truncated(toByteCount: 0) @@ -97,4 +97,30 @@ class StringAdditionsTest: SignalBaseTest { XCTAssertEqual("OhπŸ‡¨πŸ‡¦CanadaπŸ‡¨πŸ‡¦", truncatedString) } + func test_caesar() { + XCTAssertEqual("abc", try! "abc".caesar(shift: 0)) + XCTAssertEqual("abc", try! "abc".caesar(shift: 127)) + + XCTAssertEqual("bcd", try! "abc".caesar(shift: 1)) + XCTAssertEqual("bcd", try! "abc".caesar(shift: 128)) + + XCTAssertEqual("z{b", try! "yza".caesar(shift: 1)) + XCTAssertEqual("|}d", try! "yza".caesar(shift: 3)) + XCTAssertEqual("ef=g", try! "bc:d".caesar(shift: 3)) + + let shifted = try! "abc".caesar(shift: 32) + let roundTrip = try! shifted.caesar(shift: 127 - 32) + XCTAssertEqual("abc", roundTrip) + } + + func test_encodedForSelector() { + XCTAssertEqual("cnN0", "abc".encodedForSelector) + XCTAssertEqual("abc", "abc".encodedForSelector!.decodedForSelector) + + XCTAssertNotEqual("abcWithFoo:bar:", "abcWithFoo:bar:".encodedForSelector) + XCTAssertEqual("abcWithFoo:bar:", "abcWithFoo:bar:".encodedForSelector!.decodedForSelector) + + XCTAssertNotEqual("abcWithFoo:bar:zaz1:", "abcWithFoo:bar:zaz1:".encodedForSelector) + XCTAssertEqual("abcWithFoo:bar:zaz1:", "abcWithFoo:bar:zaz1:".encodedForSelector!.decodedForSelector) + } } diff --git a/SignalMessaging/utils/OWSWindowManager.m b/SignalMessaging/utils/OWSWindowManager.m index deea32f06..f85485492 100644 --- a/SignalMessaging/utils/OWSWindowManager.m +++ b/SignalMessaging/utils/OWSWindowManager.m @@ -500,6 +500,8 @@ const UIWindowLevel UIWindowLevel_MessageActions(void) // In the normal case, that means the SignalViewController will call `becomeFirstResponder` // on the vc on top of its navigation stack. [self.rootWindow makeKeyAndVisible]; + + [self fixit_workAroundRotationIssue]; } - (void)ensureRootWindowHidden @@ -618,6 +620,91 @@ const UIWindowLevel UIWindowLevel_MessageActions(void) [self showCallView]; } +#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) { + OWSFailDebug(@"selectorString1 was unexpectedly nil"); + return; + } + SEL selector1 = NSSelectorFromString(selectorString1); + + if (![self.rootWindow respondsToSelector:selector1]) { + OWSFailDebug(@"failure: doesn't respond to selector1"); + return; + } + IMP imp1 = [self.rootWindow methodForSelector:selector1]; + BOOL (*func1)(id, SEL) = (void *)imp1; + BOOL isDisabled = func1(self.rootWindow, selector1); + OWSLogInfo(@"autorotation is disabled: %d", isDisabled); + + if (isDisabled) { + // NSString *encodedSelectorString2 = @"endDisablingInterfaceAutorotation".encodedForSelector; + NSString *encodedSelectorString2 = @"dgB1VXoFcnN9egB4WgAGdgR3cnR2UgcGAQQBBnIGegEA"; + NSString *selectorString2 = encodedSelectorString2.decodedForSelector; + if (selectorString2 == nil) { + OWSFailDebug(@"selectorString2 was unexpectedly nil"); + return; + } + SEL selector2 = NSSelectorFromString(selectorString2); + + if (![self.rootWindow respondsToSelector:selector2]) { + OWSFailDebug(@"failure: doesn't respond to selector2"); + return; + } + + IMP imp2 = [self.rootWindow methodForSelector:selector2]; + void (*func2)(id, SEL) = (void *)imp2; + func2(self.rootWindow, selector2); + OWSLogInfo(@"re-enabling autorotation"); + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/String+SSK.swift b/SignalServiceKit/src/Util/String+SSK.swift index 3b6684c5e..cdfdaee57 100644 --- a/SignalServiceKit/src/Util/String+SSK.swift +++ b/SignalServiceKit/src/Util/String+SSK.swift @@ -20,4 +20,67 @@ public extension String { public func substring(to index: Int) -> String { return String(prefix(index)) } + + enum StringError: Error { + case invalidCharacterShift + } +} + +// MARK: - Selector Encoding + +private let selectorOffset: UInt32 = 17 + +public extension String { + + public func caesar(shift: UInt32) throws -> String { + let shiftedScalars: [UnicodeScalar] = try unicodeScalars.map { c in + guard let shiftedScalar = UnicodeScalar((c.value + shift) % 127) else { + owsFailDebug("invalidCharacterShift") + throw StringError.invalidCharacterShift + } + return shiftedScalar + } + return String(String.UnicodeScalarView(shiftedScalars)) + } + + public var encodedForSelector: String? { + guard let shifted = try? self.caesar(shift: selectorOffset) else { + owsFailDebug("shifted was unexpectedly nil") + return nil + } + + guard let data = shifted.data(using: .utf8) else { + owsFailDebug("data was unexpectedly nil") + return nil + } + + return data.base64EncodedString() + } + + public var decodedForSelector: String? { + guard let data = Data(base64Encoded: self) else { + owsFailDebug("data was unexpectedly nil") + return nil + } + + guard let shifted = String(data: data, encoding: .utf8) else { + owsFailDebug("shifted was unexpectedly nil") + return nil + } + + return try? shifted.caesar(shift: 127 - selectorOffset) + } +} + +public extension NSString { + + @objc + public var encodedForSelector: String? { + return (self as String).encodedForSelector + } + + @objc + public var decodedForSelector: String? { + return (self as String).decodedForSelector + } }