Add “interstitial call view” that is shown during lengthy “webrtc supported” check.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-02-07 15:47:33 -05:00
parent ea57b48490
commit c43063e1d6
6 changed files with 316 additions and 21 deletions

View file

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; };
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; };
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */; };
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
@ -602,6 +603,7 @@
341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = "<group>"; };
34535D801E256BE9008A4747 /* UIView+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OWS.h"; sourceTree = "<group>"; };
34535D811E256BE9008A4747 /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = "<group>"; };
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInterstitialViewController.swift; sourceTree = "<group>"; };
34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = "<group>"; };
34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = views/OWSAnyTouchGestureRecognizer.m; sourceTree = "<group>"; };
450873C11D9D5149006B54F2 /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = "<group>"; };
@ -2607,25 +2609,26 @@
FC3196321A08142D0094C78E /* Signals */ = {
isa = PBXGroup;
children = (
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */,
4509E79B1DD6545B0025A59F /* CallViewController.swift */,
FC3196281A067D8F0094C78E /* MessageComposeTableViewController.h */,
FC3196291A067D8F0094C78E /* MessageComposeTableViewController.m */,
FCAC963A19FEF9280046DFC5 /* SignalsViewController.h */,
FCAC963B19FEF9280046DFC5 /* SignalsViewController.m */,
FCAC964F19FF0A6E0046DFC5 /* MessagesViewController.h */,
FCAC965019FF0A6E0046DFC5 /* MessagesViewController.m */,
FC31962B1A06A2190094C78E /* FingerprintViewController.h */,
FC31962C1A06A2190094C78E /* FingerprintViewController.m */,
FCB11D911A12A4AA002F93FB /* FullImageViewController.h */,
FCB11D921A12A4AA002F93FB /* FullImageViewController.m */,
A5D0699A1A50E9CB004CB540 /* ShowGroupMembersViewController.h */,
A5D069991A50E9CB004CB540 /* ShowGroupMembersViewController.m */,
FC3196281A067D8F0094C78E /* MessageComposeTableViewController.h */,
FC3196291A067D8F0094C78E /* MessageComposeTableViewController.m */,
FCAC964F19FF0A6E0046DFC5 /* MessagesViewController.h */,
FCAC965019FF0A6E0046DFC5 /* MessagesViewController.m */,
FCFD256D1A151BCB00F4C644 /* NewGroupViewController.h */,
FCFD256E1A151BCB00F4C644 /* NewGroupViewController.m */,
FC4FA0241A1B9DC600DA100A /* SignalsNavigationController.h */,
FC4FA0251A1B9DC600DA100A /* SignalsNavigationController.m */,
452E3C8C1D935C77002A45B0 /* OWSConversationSettingsTableViewController.h */,
452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */,
A5D0699A1A50E9CB004CB540 /* ShowGroupMembersViewController.h */,
A5D069991A50E9CB004CB540 /* ShowGroupMembersViewController.m */,
FC4FA0241A1B9DC600DA100A /* SignalsNavigationController.h */,
FC4FA0251A1B9DC600DA100A /* SignalsNavigationController.m */,
FCAC963A19FEF9280046DFC5 /* SignalsViewController.h */,
FCAC963B19FEF9280046DFC5 /* SignalsViewController.m */,
);
name = Signals;
sourceTree = "<group>";
@ -3081,6 +3084,7 @@
45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */,
E197B61818BBEC1A00F073E5 /* RemoteIOAudio.m in Sources */,
B67ADDC41989FF8700E1A773 /* RPServerRequestsManager.m in Sources */,
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */,
EF764C351DB67CC5000D9A87 /* UIViewController+CameraPermissions.m in Sources */,
76EB059418170B33006006FC /* HttpManager.m in Sources */,
45CD81EF1DC030E7004C9430 /* AccountManager.swift in Sources */,

View file

@ -250,6 +250,18 @@ protocol CallServiceObserver: class {
return "CallServiceActiveCallNotification"
}
class func presentCallInterstitialNotificationName() -> String {
return "PresentCallInterstitialNotification"
}
class func dismissCallInterstitialNotificationName() -> String {
return "DismissCallInterstitialNotification"
}
class func callWasCancelledByInterstitialNotificationName() -> String {
return "CallWasCancelledByInterstitialNotification"
}
// MARK: - Service Actions
/**

View file

@ -14,11 +14,37 @@ import Foundation
let contactsManager: OWSContactsManager
let contactsUpdater: ContactsUpdater
var cancelledCallTokens: [String] = []
init(redphoneManager: PhoneManager, contactsManager: OWSContactsManager, contactsUpdater: ContactsUpdater) {
self.redphoneManager = redphoneManager
self.contactsManager = contactsManager
self.contactsUpdater = contactsUpdater
super.init()
NotificationCenter.default.addObserver(self,
selector:#selector(callWasCancelledByInterstitial),
name:Notification.Name(rawValue: CallService.callWasCancelledByInterstitialNotificationName()),
object:nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func callWasCancelledByInterstitial(notification: NSNotification) {
AssertIsOnMainThread()
let callToken = notification.object as! String
cancelCallToken(callToken)
}
func cancelCallToken(_ callToken: String) {
AssertIsOnMainThread()
cancelledCallTokens.append(callToken)
}
/**
@ -44,6 +70,10 @@ import Foundation
return self.initiateRedphoneCall(recipientId: recipientId)
}
// A temporary unique id used to identify this call during the
let callToken = NSUUID().uuidString
presentCallInterstitial(callToken)
// Since users can toggle this setting, which is only communicated during contact sync, it's easy to imagine the
// preference getting stale. Especially as users are toggling the feature to test calls. So here, we opt for a
// blocking network request *every* time we place a call to make sure we've got up to date preferences.
@ -52,6 +82,10 @@ import Foundation
// SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:self.thread.contactIdentifier];
self.contactsUpdater.lookupIdentifier(recipientId,
success: { recipient in
guard !self.cancelledCallTokens.contains(callToken) else {
Logger.error("\(self.TAG) OutboundCallInitiator aborting due to cancelled call.")
return
}
guard !Environment.getCurrent().phoneManager.hasOngoingRedphoneCall() else {
Logger.error("\(self.TAG) OutboundCallInitiator aborting due to ongoing RedPhone call.")
@ -74,6 +108,9 @@ import Foundation
failure: { error in
Logger.warn("\(self.TAG) looking up recipientId: \(recipientId) failed with error \(error)")
self.cancelCallToken(callToken)
self.dismissCallInterstitial(callToken)
let alertTitle = NSLocalizedString("UNABLE_TO_PLACE_CALL", comment:"Alert Title")
let alertController = UIAlertController(title: alertTitle, message: error.localizedDescription, preferredStyle: .alert)
@ -113,4 +150,17 @@ import Foundation
return true
}
private func presentCallInterstitial(_ callToken: String) {
AssertIsOnMainThread()
let notificationName = CallService.presentCallInterstitialNotificationName()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
}
private func dismissCallInterstitial(_ callToken: String) {
AssertIsOnMainThread()
let notificationName = CallService.dismissCallInterstitialNotificationName()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
}
}

View file

@ -0,0 +1,166 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc(OWSCallInterstitialViewController)
class CallInterstitialViewController: UIViewController {
let TAG = "[CallInterstitialViewController]"
var wasCallCancelled = false
var callToken: String?
// MARK: Views
var hasConstraints = false
var blurView: UIVisualEffectView!
var contentView: UIView!
// MARK: Initializers
required init?(coder aDecoder: NSCoder) {
assert(false)
super.init(coder: aDecoder)
}
required init() {
super.init(nibName: nil, bundle: nil)
observeNotifications()
}
func observeNotifications() {
NotificationCenter.default.addObserver(self,
selector:#selector(willResignActive),
name:NSNotification.Name.UIApplicationWillResignActive,
object:nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func willResignActive() {
cancelCall()
}
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
createViews()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
blurView.layer.opacity = 0
contentView.layer.opacity = 0
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.3,
delay: 1.0,
options: UIViewAnimationOptions.curveLinear,
animations: {
self.blurView.layer.opacity = 1
self.contentView.layer.opacity = 1
},
completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
blurView.layer.removeAllAnimations()
contentView.layer.removeAllAnimations()
}
// MARK: - Create Views
func createViews() {
assert(self.view != nil)
// Dark blurred background.
let blurEffect = UIBlurEffect(style: .dark)
blurView = UIVisualEffectView(effect: blurEffect)
blurView.isUserInteractionEnabled = false
self.view.addSubview(blurView)
contentView = UIView()
self.view.addSubview(contentView)
let dialingLabel = UILabel()
dialingLabel.text = NSLocalizedString("CALL_INTERSTITIAL_CALLING_LABEL", comment: "Title for call interstitial view")
dialingLabel.textColor = UIColor.white
dialingLabel.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(32, 40))
dialingLabel.textAlignment = .center
contentView.addSubview(dialingLabel)
let cancelCallButton = UIButton()
cancelCallButton.setTitle(NSLocalizedString("CALL_INTERSTITIAL_CANCEL_BUTTON", comment: "Label for cancel button on call interstitial view"),
for:.normal)
cancelCallButton.setTitleColor(UIColor.white, for:.normal)
cancelCallButton.titleLabel?.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(26, 32))
let buttonInset = ScaleFromIPhone5To7Plus(7, 9)
cancelCallButton.titleEdgeInsets = UIEdgeInsets(top: buttonInset,
left: buttonInset,
bottom: buttonInset,
right: buttonInset)
cancelCallButton.addTarget(self, action:#selector(cancelCallButtonPressed), for:.touchUpInside)
contentView.addSubview(cancelCallButton)
dialingLabel.autoPinWidthToSuperview()
dialingLabel.autoVCenterInSuperview()
cancelCallButton.autoSetDimension(.height, toSize:ScaleFromIPhone5To7Plus(50, 60))
cancelCallButton.autoPinWidthToSuperview()
cancelCallButton.autoPinEdge(toSuperviewEdge:.bottom, withInset:ScaleFromIPhone5To7Plus(23, 41))
}
func cancelCallButtonPressed(sender button: UIButton) {
cancelCall()
}
// MARK: - Layout
override func updateViewConstraints() {
if !hasConstraints {
// We only want to create our constraints once.
//
// Note that constraints are also created elsewhere.
// This only creates the constraints for the top-level contents of the view.
hasConstraints = true
// Force creation of the view.
let view = self.view
assert(view != nil)
// Dark blurred background.
blurView.autoPinEdgesToSuperviewEdges()
contentView.autoPinEdgesToSuperviewEdges()
}
super.updateViewConstraints()
}
// MARK: - Methods
func cancelCall() {
guard !wasCallCancelled else {
return
}
wasCallCancelled = true
assert(callToken != nil)
let notificationName = CallService.callWasCancelledByInterstitialNotificationName()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
self.dismiss(animated: false)
}
}

View file

@ -125,11 +125,19 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
(self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable)) {
[self registerForPreviewingWithDelegate:self sourceView:self.tableView];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleActiveCallNotification:)
name:[CallService callServiceActiveCallNotificationName]
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handlePresentCallInterstitialNotification:)
name:[CallService presentCallInterstitialNotificationName]
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDismissCallInterstitialNotification:)
name:[CallService dismissCallInterstitialNotificationName]
object:nil];
}
- (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
@ -150,22 +158,71 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
}
}
- (void)handleActiveCallNotification:(NSNotification *)notification
- (void)handlePresentCallInterstitialNotification:(NSNotification *)notification
{
AssertIsOnMainThread();
if (![notification.object isKindOfClass:[SignalCall class]]) {
DDLogError(@"%@ expected presentCall observer to be notified with a SignalCall, but found %@",
self.tag,
notification.object);
return;
}
SignalCall *call = (SignalCall *)notification.object;
NSString *callToken = notification.object;
OWSAssert(callToken != nil);
OWSCallInterstitialViewController *viewController = [OWSCallInterstitialViewController new];
viewController.callToken = callToken;
void(^presentInterstitial)() = ^{
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[self presentViewController:viewController
animated:NO
completion:nil];
};
// Dismiss any other modals so we can present call modal.
if (self.presentedViewController) {
[self dismissViewControllerAnimated:YES completion:^{
presentInterstitial();
}];
} else {
presentInterstitial();
}
}
- (void)handleDismissCallInterstitialNotification:(NSNotification *)notification
{
AssertIsOnMainThread();
NSString *callToken = notification.object;
OWSAssert(callToken != nil);
if (!self.presentedViewController ||
![self.presentedViewController isKindOfClass:[OWSCallInterstitialViewController class]]) {
return;
}
OWSCallInterstitialViewController *viewController = (OWSCallInterstitialViewController *)self.presentedViewController;
if (![viewController.callToken isEqualToString:callToken]) {
return;
}
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)handleActiveCallNotification:(NSNotification *)notification
{
AssertIsOnMainThread();
if (![notification.object isKindOfClass:[SignalCall class]]) {
DDLogError(@"%@ expected presentCall observer to be notified with a SignalCall, but found %@",
self.tag,
notification.object);
return;
}
SignalCall *call = (SignalCall *)notification.object;
// Dismiss any other modals so we can present call modal.
if (self.presentedViewController) {
BOOL shouldAnimate = ![self.presentedViewController isKindOfClass:[OWSCallInterstitialViewController class]];
[self dismissViewControllerAnimated:shouldAnimate
completion:^{
[self performSegueWithIdentifier:SignalsViewControllerSegueShowIncomingCall sender:call];
}];
} else {

View file

@ -70,6 +70,12 @@
/* No comment provided by engineer. */
"AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to work properly. You can grant this permission in the Settings app >> Privacy >> Microphone >> Signal";
/* Title for call interstitial view */
"CALL_INTERSTITIAL_CALLING_LABEL" = "Calling...";
/* Label for cancel button on call interstitial view */
"CALL_INTERSTITIAL_CANCEL_BUTTON" = "Cancel";
/* Accessibilty label for placing call button */
"CALL_LABEL" = "Call";