Merge branch 'charlesmchen/callVsText'

pull/1/head
Matthew Chen 7 years ago
commit 040e3b88b0

@ -12,12 +12,12 @@ GEM
atomos (0.1.2)
babosa (1.0.2)
claide (1.0.2)
cocoapods (1.4.0)
cocoapods (1.5.0)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.4.0)
cocoapods-core (= 1.5.0)
cocoapods-deintegrate (>= 1.0.2, < 2.0)
cocoapods-downloader (>= 1.1.3, < 2.0)
cocoapods-downloader (>= 1.2.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
@ -27,16 +27,16 @@ GEM
escape (~> 0.0.4)
fourflusher (~> 2.0.1)
gh_inspector (~> 1.0)
molinillo (~> 0.6.4)
molinillo (~> 0.6.5)
nap (~> 1.0)
ruby-macho (~> 1.1)
xcodeproj (>= 1.5.4, < 2.0)
cocoapods-core (1.4.0)
xcodeproj (>= 1.5.7, < 2.0)
cocoapods-core (1.5.0)
activesupport (>= 4.0.2, < 6)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.2)
cocoapods-downloader (1.1.3)
cocoapods-downloader (1.2.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
@ -100,7 +100,7 @@ GEM
xcpretty-travis-formatter (>= 0.0.3)
fourflusher (2.0.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.2)
gh_inspector (1.1.3)
google-api-client (0.13.6)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.5)
@ -134,11 +134,11 @@ GEM
mime-types-data (3.2016.0521)
mini_magick (4.5.1)
minitest (5.11.3)
molinillo (0.6.4)
molinillo (0.6.5)
multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.3)
nanaimo (0.2.5)
nap (1.1.0)
netrc (0.11.0)
os (0.9.6)
@ -172,12 +172,12 @@ GEM
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
word_wrap (1.0.0)
xcodeproj (1.5.6)
CFPropertyList (~> 2.3.3)
xcodeproj (1.5.7)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.2)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.3)
nanaimo (~> 0.2.4)
xcpretty (0.2.8)
rouge (~> 2.0.7)
xcpretty-travis-formatter (0.0.4)

@ -1 +1 @@
Subproject commit 2913d54307377858992ee731984fd662a438cab7
Subproject commit 99c60d31efccb927361f1aa0c3d2cca238ca515c

@ -142,6 +142,7 @@
34612A011FD5F31400532771 /* OWS104CreateRecipientIdentities.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129F41FD5F31400532771 /* OWS104CreateRecipientIdentities.h */; };
34612A061FD7238600532771 /* OWSContactsSyncing.h in Headers */ = {isa = PBXBuildFile; fileRef = 34612A041FD7238500532771 /* OWSContactsSyncing.h */; settings = {ATTRIBUTES = (Public, ); }; };
34612A071FD7238600532771 /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34612A051FD7238500532771 /* OWSContactsSyncing.m */; };
34641E1220878FB000E2EDE5 /* OWSWindowManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */; };
34641E182088D7E900E2EDE5 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */; };
34641E1B2088DA4100E2EDE5 /* ScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E192088DA3F00E2EDE5 /* ScreenLockViewController.m */; };
34641E1C2088DA4100E2EDE5 /* ScreenLockViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 34641E1A2088DA4000E2EDE5 /* ScreenLockViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -730,6 +731,8 @@
346129F41FD5F31400532771 /* OWS104CreateRecipientIdentities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS104CreateRecipientIdentities.h; sourceTree = "<group>"; };
34612A041FD7238500532771 /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = "<group>"; };
34612A051FD7238500532771 /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = "<group>"; };
34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSWindowManager.m; sourceTree = "<group>"; };
34641E1120878FB000E2EDE5 /* OWSWindowManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSWindowManager.h; sourceTree = "<group>"; };
34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSScreenLock.swift; sourceTree = "<group>"; };
34641E192088DA3F00E2EDE5 /* ScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ScreenLockViewController.m; path = SignalMessaging/ViewControllers/ScreenLockViewController.m; sourceTree = SOURCE_ROOT; };
34641E1A2088DA4000E2EDE5 /* ScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ScreenLockViewController.h; path = SignalMessaging/ViewControllers/ScreenLockViewController.h; sourceTree = SOURCE_ROOT; };
@ -2045,9 +2048,11 @@
340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */,
340FC8CB20518C76007AEB0F /* OWSBackupJob.h */,
340FC8CC20518C76007AEB0F /* OWSBackupJob.m */,
34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */,
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */,
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */,
34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */,
34641E1120878FB000E2EDE5 /* OWSWindowManager.h */,
34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */,
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
4579431D1E7C8CE9008ED0C0 /* Pastelog.m */,
450DF2041E0D74AC003D14BE /* Platform.swift */,
@ -3201,6 +3206,7 @@
45D308AD2049A439000189E4 /* PinEntryView.m in Sources */,
340FC8B1204DAC8D007AEB0F /* BlockListViewController.m in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
34641E1220878FB000E2EDE5 /* OWSWindowManager.m in Sources */,
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,

@ -189,6 +189,9 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
}
[OWSScreenLockUI.sharedManager setupWithRootWindow:self.window];
[[OWSWindowManager sharedManager] setupWithRootWindow:self.window
screenBlockingWindow:OWSScreenLockUI.sharedManager.screenBlockingWindow];
[OWSScreenLockUI.sharedManager startObserving];
// Ensure OWSContactsSyncing is instantiated.
[OWSContactsSyncing sharedManager];

@ -31,6 +31,7 @@
#import "OWSProgressView.h"
#import "OWSQuotedMessageView.h"
#import "OWSWebRTCDataProtos.pb.h"
#import "OWSWindowManager.h"
#import "PinEntryView.h"
#import "PrivacySettingsTableViewController.h"
#import "ProfileViewController.h"

@ -19,21 +19,24 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
return SignalApp.shared().callUIAdapter
}
// Feature Flag
@objc public static let kShowCallViewOnSeparateWindow = true
let contactsManager: OWSContactsManager
// MARK: Properties
// MARK: - Properties
let thread: TSContactThread
let call: SignalCall
var hasDismissed = false
// MARK: Views
// MARK: - Views
var hasConstraints = false
var blurView: UIVisualEffectView!
var dateFormatter: DateFormatter?
// MARK: Contact Views
// MARK: - Contact Views
var contactNameLabel: MarqueeLabel!
var contactAvatarView: AvatarImageView!
@ -41,7 +44,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
var callStatusLabel: UILabel!
var callDurationTimer: Timer?
// MARK: Ongoing Call Controls
// MARK: - Ongoing Call Controls
var ongoingCallView: UIView!
@ -56,14 +59,14 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
// call.
// var textMessageButton: UIButton!
// MARK: Incoming Call Controls
// MARK: - Incoming Call Controls
var incomingCallView: UIView!
var acceptIncomingButton: UIButton!
var declineIncomingButton: UIButton!
// MARK: Video Views
// MARK: - Video Views
var remoteVideoView: RemoteVideoView!
var localVideoView: RTCCameraPreviewView!
@ -81,7 +84,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
}
// MARK: Settings Nag Views
// MARK: - Settings Nag Views
var isShowingSettingsNag = false {
didSet {
@ -93,7 +96,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
var settingsNagView: UIView!
var settingsNagDescriptionLabel: UILabel!
// MARK: Audio Source
// MARK: - Audio Source
var hasAlternateAudioSources: Bool {
Logger.info("\(TAG) available audio sources: \(allAudioSources)")
@ -126,7 +129,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
}
// MARK: Initializers
// MARK: - Initializers
@available(*, unavailable, message: "use init(call:) constructor instead.")
required init?(coder aDecoder: NSCoder) {
@ -152,7 +155,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
}
}
// MARK: View Lifecycle
// MARK: - View Lifecycle
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
@ -407,6 +410,9 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
actionSheetController.addAction(routeAudioAction)
}
// Note: It's critical that we present from this view and
// not the "frontmost view controller" since this view may
// reside on a separate window.
self.present(actionSheetController, animated: true)
}
@ -928,7 +934,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
dismissIfPossible(shouldDelay: false, ignoreNag: true, completion: {
// Find the frontmost presented UIViewController from which to present the
// settings views.
let fromViewController = UIApplication.shared.frontmostViewController
let fromViewController = UIApplication.shared.findFrontmostViewController(ignoringAlerts: true)
assert(fromViewController != nil)
// Construct the "settings" view & push the "privacy settings" view.
@ -962,6 +968,13 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
preferences.setIsCallKitPrivacyEnabled(preferences.isCallKitPrivacyEnabled())
}
func didTapLeaveCall(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
OWSWindowManager.shared().leaveCallView()
}
// MARK: - CallObserver
internal func stateDidChange(call: SignalCall, state: CallState) {
@ -990,7 +1003,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
self.updateCallUI(callState: call.state)
}
// MARK: CallAudioServiceDelegate
// MARK: - CallAudioServiceDelegate
func callAudioService(_ callAudioService: CallAudioService, didUpdateIsSpeakerphoneEnabled isSpeakerphoneEnabled: Bool) {
SwiftAssertIsOnMainThread(#function)
@ -1103,11 +1116,20 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver,
hasDismissed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.dismiss(animated: true, completion: completion)
strongSelf.dismissImmediately(completion: completion)
}
} else {
hasDismissed = true
self.dismiss(animated: false, completion: completion)
dismissImmediately(completion: completion)
}
}
internal func dismissImmediately(completion: (() -> Void)?) {
if CallViewController.kShowCallViewOnSeparateWindow {
OWSWindowManager.shared().endCall(self)
completion?()
} else {
self.dismiss(animated: true, completion: completion)
}
}

@ -1687,11 +1687,9 @@ protocol CallServiceObserver: class {
return
}
let frontmostViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts
guard nil != frontmostViewController as? CallViewController else {
if !OWSWindowManager.shared().hasCall() {
OWSProdError(OWSAnalyticsEvents.callServiceCallViewCouldNotPresent(), file: #file, function: #function, line: #line)
owsFail("\(self.logTag) in \(#function) Call terminated due to call view presentation delay: \(frontmostViewController.debugDescription).")
owsFail("\(self.logTag) in \(#function) Call terminated due to missing call view.")
self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "Call view didn't present after \(kMaxViewPresentationDelay) seconds"))
return
}

@ -39,15 +39,22 @@ extension CallUIAdaptee {
let callViewController = CallViewController(call: call)
callViewController.modalTransitionStyle = .crossDissolve
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
owsFail("in \(#function) view controller unexpectedly nil")
return
}
if let presentedViewController = presentingViewController.presentedViewController {
presentedViewController.dismiss(animated: false)
if CallViewController.kShowCallViewOnSeparateWindow {
OWSWindowManager.shared().startCall(callViewController)
} else {
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
owsFail("in \(#function) view controller unexpectedly nil")
return
}
if let presentedViewController = presentingViewController.presentedViewController {
presentedViewController.dismiss(animated: false) {
presentingViewController.present(callViewController, animated: true)
}
} else {
presentingViewController.present(callViewController, animated: true)
}
}
presentingViewController.present(callViewController, animated: true)
}
internal func reportMissedCall(_ call: SignalCall, callerName: String) {

@ -6,12 +6,16 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSScreenLockUI : NSObject
@property (nonatomic, readonly) UIWindow *screenBlockingWindow;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
- (void)setupWithRootWindow:(UIWindow *)rootWindow;
- (void)startObserving;
@end
NS_ASSUME_NONNULL_END

@ -3,6 +3,7 @@
//
#import "OWSScreenLockUI.h"
#import "OWSWindowManager.h"
#import "Signal-Swift.h"
#import <SignalMessaging/ScreenLockViewController.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
@ -10,11 +11,10 @@
NS_ASSUME_NONNULL_BEGIN
const UIWindowLevel UIWindowLevel_Background = -1.f;
@interface OWSScreenLockUI () <ScreenLockViewDelegate>
@property (nonatomic) UIWindow *screenBlockingWindow;
@property (nonatomic) ScreenLockViewController *screenBlockingViewController;
// Unlike UIApplication.applicationState, this state reflects the
// notifications, i.e. "did become active", "will resign active",
@ -29,7 +29,6 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
// inactive in order for it to be reflected in the app switcher.
@property (nonatomic) BOOL appIsInactiveOrBackground;
@property (nonatomic) BOOL appIsInBackground;
@property (nonatomic) ScreenLockViewController *screenBlockingViewController;
@property (nonatomic) BOOL isShowingScreenLockUI;
@ -52,11 +51,6 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
// The "countdown" until screen lock takes effect.
@property (nonatomic, nullable) NSDate *screenLockCountdownDate;
@property (nonatomic) UIWindow *rootWindow;
@property (nonatomic, nullable) UIResponder *rootWindowResponder;
@property (nonatomic, nullable, weak) UIViewController *rootFrontmostViewController;
@end
#pragma mark -
@ -83,10 +77,6 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
OWSAssertIsOnMainThread();
_appIsInactiveOrBackground = [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
[self observeNotifications];
OWSSingletonAssert();
return self;
@ -130,9 +120,18 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
OWSAssertIsOnMainThread();
OWSAssert(rootWindow);
self.rootWindow = rootWindow;
[self createScreenBlockingWindowWithRootWindow:rootWindow];
OWSAssert(self.screenBlockingWindow);
}
- (void)startObserving
{
_appIsInactiveOrBackground = [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
[self observeNotifications];
[self prepareScreenProtectionWithRootWindow:rootWindow];
// Default to screen protection until we know otherwise.
[self updateScreenBlockingWindow:ScreenLockUIStateScreenProtection animated:NO];
// Initialize the screen lock state.
//
@ -372,14 +371,14 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
// After the alert, update the UI.
[self ensureUI];
}
fromViewController:self.screenBlockingViewController];
fromViewController:self.screenBlockingWindow.rootViewController];
}
// 'Screen Blocking' window obscures the app screen:
//
// * In the app switcher.
// * During 'Screen Lock' unlock process.
- (void)prepareScreenProtectionWithRootWindow:(UIWindow *)rootWindow
- (void)createScreenBlockingWindowWithRootWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssert(rootWindow);
@ -396,9 +395,6 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
self.screenBlockingWindow = window;
self.screenBlockingViewController = viewController;
// Default to screen protection until we know otherwise.
[self updateScreenBlockingWindow:ScreenLockUIStateNone animated:NO];
}
// The "screen blocking" window has three possible states:
@ -413,59 +409,8 @@ const UIWindowLevel UIWindowLevel_Background = -1.f;
OWSAssertIsOnMainThread();
BOOL shouldShowBlockWindow = desiredUIState != ScreenLockUIStateNone;
if (self.rootWindow.hidden != shouldShowBlockWindow) {
DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window");
}
// When we show the block window, try to capture the first responder of
// the root window before it is hidden.
//
// When we hide the root window, its first responder will resign.
if (shouldShowBlockWindow && !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]);
}
// * Show/hide the app's root window as necessary.
// * 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.
if (shouldShowBlockWindow) {
// Show the blocking window in front of the status bar.
self.screenBlockingWindow.windowLevel = UIWindowLevelStatusBar + 1;
self.rootWindow.hidden = YES;
} else {
self.screenBlockingWindow.windowLevel = UIWindowLevel_Background;
[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.
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];
}
self.rootWindowResponder = nil;
self.rootFrontmostViewController = nil;
}
[OWSWindowManager.sharedManager setIsScreenBlockActive:shouldShowBlockWindow];
[self.screenBlockingViewController updateUIWithState:desiredUIState
isLogoAtTop:self.isShowingScreenLockUI

@ -0,0 +1,37 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// This VC can become first responder
// when presented to ensure that the input accessory is updated.
@interface OWSWindowRootViewController : UIViewController
@end
#pragma mark -
extern const UIWindowLevel UIWindowLevel_Background;
@interface OWSWindowManager : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
- (void)setupWithRootWindow:(UIWindow *)rootWindow screenBlockingWindow:(UIWindow *)screenBlockingWindow;
- (void)setIsScreenBlockActive:(BOOL)isScreenBlockActive;
#pragma mark - Calls
- (void)startCall:(UIViewController *)callViewController;
- (void)endCall:(UIViewController *)callViewController;
- (void)leaveCallView;
- (void)returnToCallView;
- (BOOL)hasCall;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,518 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSWindowManager.h"
#import "Signal-Swift.h"
#import <SignalMessaging/UIColor+OWS.h>
#import <SignalMessaging/UIFont+OWS.h>
#import <SignalMessaging/UIView+OWS.h>
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;
}
@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 updateReturnToCallWindowLayout];
[self ensureWindowState];
}
- (UIWindow *)createReturnToCallWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssert(rootWindow);
DDLogVerbose(@"%@ updateReturnToCallWindowLayout", self.logTag);
CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame;
DDLogVerbose(@"%@ statusBarFrame: %@", self.logTag, NSStringFromCGRect(statusBarFrame));
// "Return to call" should remain at the top of the screen.
CGRect windowFrame = rootWindow.bounds;
// Use zero height until updateReturnToCallWindowLayout.
windowFrame.size.height = 0;
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:)]];
rootView.layoutMargins = UIEdgeInsetsZero;
UILabel *label = [UILabel new];
label.text = NSLocalizedString(@"CALL_WINDOW_RETURN_TO_CALL", @"Label for the 'return to call' banner.");
label.textColor = [UIColor whiteColor];
// System UI doesn't use dynamic type; neither do we.
label.font = [UIFont ows_regularFontWithSize:14.f];
[rootView addSubview:label];
// returnToCallLabel uses manual layout.
//
// TODO: Is there a better way to do this?
label.translatesAutoresizingMaskIntoConstraints = NO;
self.returnToCallLabel = label;
window.rootViewController = viewController;
return window;
}
- (void)updateReturnToCallWindowLayout
{
OWSAssertIsOnMainThread();
CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame;
DDLogVerbose(@"%@ statusBarFrame: %@", self.logTag, NSStringFromCGRect(statusBarFrame));
CGFloat statusBarHeight = statusBarFrame.size.height;
CGRect windowFrame = self.rootWindow.bounds;
windowFrame.size.height = statusBarHeight + 20.f;
DDLogVerbose(@"%@ windowFrame: %@", self.logTag, NSStringFromCGRect(windowFrame));
self.returnToCallWindow.frame = windowFrame;
self.returnToCallWindow.rootViewController.view.frame = windowFrame;
[self.returnToCallLabel sizeToFit];
CGRect labelFrame = self.returnToCallLabel.frame;
labelFrame.origin.x = floor(windowFrame.size.width - labelFrame.size.width);
self.returnToCallLabel.frame = labelFrame;
UIView *rootView = self.returnToCallWindow.rootViewController.view;
[rootView setNeedsLayout];
[rootView layoutIfNeeded];
[self.returnToCallWindow logFrameWithLabel:@"returnToCallWindow"];
[rootView logFrameWithLabel:@"returnToCallWindow view"];
for (UIView *subview in rootView.subviews) {
[subview logFrameWithLabel:@"returnToCallWindow subview"];
[subview setNeedsLayout];
[subview layoutIfNeeded];
}
}
- (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;
OWSAssert(!self.callNavigationController);
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 updateReturnToCallWindowLayout];
[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);
// 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.
[self ensureRootWindowHidden];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowHidden];
[self ensureScreenBlockWindowShown];
} else if (self.callViewController && self.isCallViewActive) {
// Show Call View.
[self ensureRootWindowHidden];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowShown];
[self ensureScreenBlockWindowHidden];
} else if (self.callViewController) {
// Show Root Window + "Return to Call".
[self ensureRootWindowShown];
[self ensureReturnToCallWindowShown];
[self ensureCallViewWindowHidden];
[self ensureScreenBlockWindowHidden];
} else {
// Show Root Window
[self ensureRootWindowShown];
[self ensureReturnToCallWindowHidden];
[self ensureCallViewWindowHidden];
[self ensureScreenBlockWindowHidden];
}
}
- (void)ensureRootWindowShown
{
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)ensureRootWindowHidden
{
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)ensureReturnToCallWindowShown
{
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;
[self updateReturnToCallWindowLayout];
}
- (void)ensureReturnToCallWindowHidden
{
OWSAssertIsOnMainThread();
if (!self.returnToCallWindow.hidden) {
DDLogInfo(@"%@ hiding 'return to call' window.", self.logTag);
}
self.returnToCallWindow.hidden = YES;
[self.returnToCallLabel.layer removeAllAnimations];
}
- (void)ensureCallViewWindowShown
{
OWSAssertIsOnMainThread();
if (self.callViewWindow.hidden) {
DDLogInfo(@"%@ showing call window.", self.logTag);
}
[self.callViewWindow makeKeyAndVisible];
[self.callViewWindow.rootViewController becomeFirstResponder];
}
- (void)ensureCallViewWindowHidden
{
OWSAssertIsOnMainThread();
if (!self.callViewWindow.hidden) {
DDLogInfo(@"%@ hiding call window.", self.logTag);
}
self.callViewWindow.hidden = YES;
}
- (void)ensureScreenBlockWindowShown
{
OWSAssertIsOnMainThread();
if (self.screenBlockingWindow.windowLevel != UIWindowLevel_ScreenBlocking()) {
DDLogInfo(@"%@ showing block window.", self.logTag);
}
self.screenBlockingWindow.windowLevel = UIWindowLevel_ScreenBlocking();
[self.screenBlockingWindow.rootViewController becomeFirstResponder];
}
- (void)ensureScreenBlockWindowHidden
{
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

@ -319,6 +319,9 @@
/* Accessibility label to switch to video call */
"CALL_VIEW_SWITCH_TO_VIDEO_LABEL" = "Switch to video call";
/* Label for the 'return to call' indicator. */
"CALL_WINDOW_RETURN_TO_CALL" = "Touch to return to call";
/* notification action */
"CALLBACK_BUTTON_TITLE" = "Call Back";

@ -136,4 +136,9 @@ NSString *NSStringForScreenLockUIState(ScreenLockUIState value)
[self.delegate unlockButtonWasTapped];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end

Loading…
Cancel
Save