Chromium Code Reviews| Index: remoting/ios/app/client_connection_view_controller.mm |
| diff --git a/remoting/ios/app/client_connection_view_controller.mm b/remoting/ios/app/client_connection_view_controller.mm |
| index 888ee2c358ea998c6de2cf1d4f89f90508397ad5..8e4c8a57a73bdf2a44f47a123ca6a47c1cfbbe79 100644 |
| --- a/remoting/ios/app/client_connection_view_controller.mm |
| +++ b/remoting/ios/app/client_connection_view_controller.mm |
| @@ -12,9 +12,11 @@ |
| #import "ios/third_party/material_components_ios/src/components/ActivityIndicator/src/MDCActivityIndicator.h" |
| #import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h" |
| #import "ios/third_party/material_components_ios/src/components/NavigationBar/src/MaterialNavigationBar.h" |
| +#import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h" |
| #import "remoting/ios/app/host_view_controller.h" |
| #import "remoting/ios/app/pin_entry_view.h" |
| #import "remoting/ios/app/remoting_theme.h" |
| +#import "remoting/ios/app/session_reconnect_view.h" |
| #import "remoting/ios/domain/client_session_details.h" |
| #import "remoting/ios/domain/host_info.h" |
| #import "remoting/ios/facade/remoting_authentication.h" |
| @@ -31,7 +33,10 @@ static const CGFloat kActivityIndicatorRadius = 33.f; |
| static const CGFloat kPinEntryViewWidth = 240.f; |
| static const CGFloat kPinEntryViewHeight = 90.f; |
| -static const CGFloat kCenterShift = -80.f; |
| +static const CGFloat kReconnectViewWidth = 120.f; |
| +static const CGFloat kReconnectViewHeight = 90.f; |
| + |
| +static const CGFloat kTopPadding = 240.f; |
| static const CGFloat kPadding = 20.f; |
| static const CGFloat kMargin = 20.f; |
| @@ -39,14 +44,20 @@ static const CGFloat kBarHeight = 58.f; |
| static const CGFloat kKeyboardAnimationTime = 0.3; |
| -@interface ClientConnectionViewController ()<PinEntryDelegate> { |
| +@interface ClientConnectionViewController ()<PinEntryDelegate, |
| + SessionReconnectViewDelegate> { |
| UIImageView* _iconView; |
| MDCActivityIndicator* _activityIndicator; |
| + NSLayoutConstraint* _activityIndicatorTopConstraintFull; |
| + NSLayoutConstraint* _activityIndicatorTopConstraintKeyboard; |
| UILabel* _statusLabel; |
| MDCNavigationBar* _navBar; |
| PinEntryView* _pinEntryView; |
| + SessionReconnectView* _reconnectView; |
| NSString* _remoteHostName; |
| RemotingClient* _client; |
| + SessionErrorCode _lastError; |
| + HostInfo* _hostInfo; |
| } |
| @end |
| @@ -57,17 +68,7 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| - (instancetype)initWithHostInfo:(HostInfo*)hostInfo { |
| self = [super init]; |
| if (self) { |
| - _client = [[RemotingClient alloc] init]; |
| - |
| - __weak RemotingClient* weakClient = _client; |
| - [RemotingService.instance.authentication |
| - callbackWithAccessToken:^(RemotingAuthenticationStatus status, |
| - NSString* userEmail, NSString* accessToken) { |
| - [weakClient connectToHost:hostInfo |
| - username:userEmail |
| - accessToken:accessToken]; |
| - }]; |
| - |
| + _hostInfo = hostInfo; |
| _remoteHostName = hostInfo.hostName; |
| // TODO(yuweih): This logic may be reused by other views. |
| @@ -102,73 +103,161 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| constraintEqualToAnchor:[self.view trailingAnchor]], |
| [[_navBar heightAnchor] constraintEqualToConstant:kBarHeight], |
| ]]; |
| + |
| + [self attemptConnectionToHost]; |
| } |
| return self; |
| } |
| #pragma mark - UIViewController |
| -- (void)loadView { |
| - [super loadView]; |
| - |
| +- (void)viewDidLoad { |
| + [super viewDidLoad]; |
| self.view.backgroundColor = RemotingTheme.connectionViewBackgroundColor; |
| _activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectZero]; |
| + _activityIndicator.radius = kActivityIndicatorRadius; |
| + _activityIndicator.trackEnabled = YES; |
| + _activityIndicator.strokeWidth = kActivityIndicatorStrokeWidth; |
| + _activityIndicator.cycleColors = @[ UIColor.whiteColor ]; |
| + _activityIndicator.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_activityIndicator]; |
| _statusLabel = [[UILabel alloc] initWithFrame:CGRectZero]; |
| + _statusLabel.numberOfLines = 1; |
| + _statusLabel.lineBreakMode = NSLineBreakByTruncatingTail; |
| + _statusLabel.textColor = [UIColor whiteColor]; |
| + _statusLabel.textAlignment = NSTextAlignmentCenter; |
| + _statusLabel.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_statusLabel]; |
| _iconView = [[UIImageView alloc] initWithFrame:CGRectZero]; |
| - [self.view addSubview:_iconView]; |
| - |
| - _pinEntryView = [[PinEntryView alloc] init]; |
| - [self.view addSubview:_pinEntryView]; |
| - _pinEntryView.delegate = self; |
| -} |
| - |
| -- (void)viewDidLoad { |
| - [super viewDidLoad]; |
| - |
| _iconView.contentMode = UIViewContentModeCenter; |
| _iconView.alpha = 0.87f; |
| _iconView.backgroundColor = RemotingTheme.onlineHostColor; |
| _iconView.layer.cornerRadius = kIconRadius; |
| _iconView.layer.masksToBounds = YES; |
| _iconView.image = RemotingTheme.desktopIcon; |
| + _iconView.translatesAutoresizingMaskIntoConstraints = NO; |
| + [self.view addSubview:_iconView]; |
| - _activityIndicator.radius = kActivityIndicatorRadius; |
| - _activityIndicator.trackEnabled = YES; |
| - _activityIndicator.strokeWidth = kActivityIndicatorStrokeWidth; |
| - _activityIndicator.cycleColors = @[ UIColor.whiteColor ]; |
| - |
| - _statusLabel.numberOfLines = 1; |
| - _statusLabel.lineBreakMode = NSLineBreakByTruncatingTail; |
| - _statusLabel.textColor = [UIColor whiteColor]; |
| - _statusLabel.textAlignment = NSTextAlignmentCenter; |
| + _reconnectView = [[SessionReconnectView alloc] initWithFrame:CGRectZero]; |
| + _reconnectView.hidden = YES; |
| + _reconnectView.translatesAutoresizingMaskIntoConstraints = NO; |
| + [self.view addSubview:_reconnectView]; |
| + _reconnectView.delegate = self; |
| + _pinEntryView = [[PinEntryView alloc] init]; |
| _pinEntryView.hidden = YES; |
| -} |
| - |
| -- (void)viewWillLayoutSubviews { |
| - [super viewWillLayoutSubviews]; |
| + _pinEntryView.translatesAutoresizingMaskIntoConstraints = NO; |
| + [self.view addSubview:_pinEntryView]; |
| + _pinEntryView.delegate = self; |
| - _iconView.frame = CGRectMake(0, 0, kIconRadius * 2, kIconRadius * 2.f); |
| - _iconView.center = |
| - CGPointMake(self.view.center.x, self.view.center.y + kCenterShift); |
| + [self |
| + initializeLayoutConstraintsWithViews:NSDictionaryOfVariableBindings( |
| + _activityIndicator, _statusLabel, |
| + _iconView, _reconnectView, |
| + _pinEntryView)]; |
| +} |
| +- (void)initializeLayoutConstraintsWithViews:(NSDictionary*)views { |
| + // Metrics to use in visual format strings. |
| + NSDictionary* layoutMetrics = @{ |
| + @"padding" : @(kPadding), |
| + @"margin" : @(kMargin), |
| + @"topPadding" : @(kTopPadding), |
| + @"iconDiameter" : @(kIconRadius * 2), |
| + @"pinEntryViewWidth" : @(kPinEntryViewWidth), |
| + @"pinEntryViewHeight" : @(kPinEntryViewHeight), |
| + @"reconnectViewWidth" : @(kReconnectViewWidth), |
| + @"reconnectViewHeight" : @(kReconnectViewHeight), |
| + }; |
| [_activityIndicator sizeToFit]; |
| - _activityIndicator.center = _iconView.center; |
| - |
| - _statusLabel.frame = |
| - CGRectMake(kMargin, _activityIndicator.center.y + kIconRadius + kPadding, |
| - self.view.frame.size.width - kMargin * 2.f, |
| - _statusLabel.font.pointSize * _statusLabel.numberOfLines); |
| - |
| - _pinEntryView.frame = CGRectMake( |
| - (self.view.frame.size.width - kPinEntryViewWidth) / 2.f, |
| - _statusLabel.frame.origin.y + _statusLabel.frame.size.height + kPadding, |
| - kPinEntryViewWidth, kPinEntryViewHeight); |
| + NSString* f; |
| + |
| + // Horizontal constraints: |
| + |
| + [self.view addConstraints: |
|
Yuwei
2017/07/06 21:52:39
The doc says [NSLayoutConstraint activateConstrain
nicholss
2017/07/06 22:15:05
arg. :(
It looks like people are all over the pla
|
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat:@"H:[_iconView(iconDiameter)]" |
| + options:0 |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view addConstraints:[NSLayoutConstraint |
| + constraintsWithVisualFormat: |
| + @"H:|-margin-[_statusLabel]-margin-|" |
| + options:0 |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view addConstraints:[NSLayoutConstraint |
| + constraintsWithVisualFormat: |
| + @"H:[_pinEntryView(pinEntryViewWidth)]" |
| + options:0 |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view addConstraints:[NSLayoutConstraint |
| + constraintsWithVisualFormat: |
| + @"H:[_reconnectView(reconnectViewWidth)]" |
| + options:0 |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + // Anchors: |
| + |
| + _activityIndicatorTopConstraintFull = |
| + [_activityIndicator.topAnchor constraintEqualToAnchor:self.view.topAnchor |
| + constant:kTopPadding]; |
| + _activityIndicatorTopConstraintFull.active = YES; |
| + |
| + [_iconView.centerYAnchor |
| + constraintEqualToAnchor:_activityIndicator.centerYAnchor] |
| + .active = YES; |
| + |
| + // Vertical constraints: |
| + |
| + [self.view addConstraints: |
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat:@"V:[_iconView(iconDiameter)]" |
| + options:0 |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view addConstraints: |
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat: |
| + @"V:[_activityIndicator]-(padding)-[_statusLabel]" |
| + options:NSLayoutFormatAlignAllCenterX |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view addConstraints: |
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat: |
| + @"V:[_iconView]-(padding)-[_statusLabel]" |
| + options:NSLayoutFormatAlignAllCenterX |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + f = @"V:[_statusLabel]-(padding)-[_pinEntryView(pinEntryViewHeight)]"; |
| + [self.view addConstraints: |
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat:f |
| + options:NSLayoutFormatAlignAllCenterX |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + f = @"V:[_statusLabel]-padding-[_reconnectView(reconnectViewHeight)]"; |
| + [self.view addConstraints: |
| + [NSLayoutConstraint |
| + constraintsWithVisualFormat:f |
| + options:NSLayoutFormatAlignAllCenterX |
| + metrics:layoutMetrics |
| + views:views]]; |
| + |
| + [self.view setNeedsUpdateConstraints]; |
|
Yuwei
2017/07/06 21:52:39
You probably don't need this if you use activateCo
nicholss
2017/07/06 22:15:05
I tried it out and you still need to call [self se
nicholss
2017/07/06 22:15:06
no that is for bulk activation of an array of cons
Yuwei
2017/07/06 22:36:28
Okay... Sad...
I don't remember I have ever used
|
| } |
| - (void)viewWillAppear:(BOOL)animated { |
| @@ -217,29 +306,30 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| CGRectValue] |
| .size; |
| - [UIView |
| - animateWithDuration:kKeyboardAnimationTime |
| - animations:^{ |
| - CGRect f = self.view.frame; |
| - CGFloat newHeight = |
| - self.view.frame.size.height - keyboardSize.height; |
| - CGFloat overlap = |
| - newHeight - (_pinEntryView.frame.origin.y + |
| - _pinEntryView.frame.size.height + kPadding); |
| - if (overlap < 0) { |
| - f.origin.y = overlap; |
| - // TODO(yuweih): This may push the navigation bar off screen. |
| - self.view.frame = f; |
| - } |
| - }]; |
| + CGFloat newHeight = self.view.frame.size.height - keyboardSize.height; |
| + CGFloat overlap = newHeight - (_pinEntryView.frame.origin.y + |
| + _pinEntryView.frame.size.height + kPadding); |
| + if (overlap > 0) { |
| + overlap = 0; |
| + } |
| + _activityIndicatorTopConstraintKeyboard.active = NO; |
| + _activityIndicatorTopConstraintKeyboard = [_activityIndicator.topAnchor |
| + constraintEqualToAnchor:self.view.topAnchor |
| + constant:kTopPadding + overlap]; |
| + _activityIndicatorTopConstraintFull.active = NO; |
| + _activityIndicatorTopConstraintKeyboard.active = YES; |
| + [UIView animateWithDuration:kKeyboardAnimationTime |
| + animations:^{ |
| + [self.view layoutIfNeeded]; |
| + }]; |
| } |
| - (void)keyboardWillHide:(NSNotification*)notification { |
| + _activityIndicatorTopConstraintKeyboard.active = NO; |
| + _activityIndicatorTopConstraintFull.active = YES; |
| [UIView animateWithDuration:kKeyboardAnimationTime |
| animations:^{ |
| - CGRect f = self.view.frame; |
| - f.origin.y = 0.f; |
| - self.view.frame = f; |
| + [self.view layoutIfNeeded]; |
| }]; |
| } |
| @@ -258,13 +348,36 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| [self showConnectedState]; |
| break; |
| case ClientViewClosed: |
| - [self dismissViewControllerAnimated:YES completion:nil]; |
| + [self.navigationController popToRootViewControllerAnimated:YES]; |
| + break; |
| + case ClientViewError: |
| + [self showError]; |
| break; |
| } |
| } |
| +#pragma mark - SessionReconnectViewDelegate |
| + |
| +- (void)didTapReconnect { |
| + [self attemptConnectionToHost]; |
| +} |
| + |
| #pragma mark - Private |
| +- (void)attemptConnectionToHost { |
| + _client = [[RemotingClient alloc] init]; |
| + __weak RemotingClient* weakClient = _client; |
| + __weak HostInfo* weakHostInfo = _hostInfo; |
| + [RemotingService.instance.authentication |
| + callbackWithAccessToken:^(RemotingAuthenticationStatus status, |
| + NSString* userEmail, NSString* accessToken) { |
| + [weakClient connectToHost:weakHostInfo |
| + username:userEmail |
| + accessToken:accessToken]; |
| + }]; |
| + [self setState:ClientViewConnecting]; |
| +} |
| + |
| - (void)showConnectingState { |
| [_pinEntryView endEditing:YES]; |
| _statusLabel.text = |
| @@ -274,6 +387,7 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| _activityIndicator.indicatorMode = MDCActivityIndicatorModeIndeterminate; |
| _activityIndicator.hidden = NO; |
| _pinEntryView.hidden = YES; |
| + _reconnectView.hidden = YES; |
| [_activityIndicator startAnimating]; |
| } |
| @@ -282,6 +396,7 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| [_activityIndicator stopAnimating]; |
| _activityIndicator.hidden = YES; |
| _pinEntryView.hidden = NO; |
| + _reconnectView.hidden = YES; |
| // TODO(yuweih): This may be called before viewDidAppear and miss the keyboard |
| // callback. |
| @@ -299,6 +414,7 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| _activityIndicator.cycleColors = @[ [UIColor greenColor] ]; |
| [_activityIndicator startAnimating]; |
| _activityIndicator.progress = 1.0; |
| + _reconnectView.hidden = YES; |
| HostViewController* hostViewController = |
| [[HostViewController alloc] initWithClient:_client]; |
| @@ -312,6 +428,77 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| [self.navigationController setViewControllers:controllers animated:NO]; |
| } |
| +- (void)showError { |
| + _statusLabel.text = |
| + [NSString stringWithFormat:@"Error connecting to %@", _remoteHostName]; |
| + _activityIndicator.progress = 0.0; |
| + _pinEntryView.hidden = YES; |
| + _activityIndicator.hidden = NO; |
| + _activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate; |
| + _activityIndicator.cycleColors = @[ [UIColor redColor] ]; |
| + [_activityIndicator startAnimating]; |
| + _activityIndicator.progress = 1.0; |
| + _reconnectView.hidden = NO; |
| + |
| + MDCSnackbarMessage* message = nil; |
| + switch (_lastError) { |
| + case SessionErrorOk: |
| + // Do nothing. |
| + break; |
| + case SessionErrorPeerIsOffline: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorPeerIsOffline."]; |
| + break; |
| + case SessionErrorSessionRejected: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorSessionRejected."]; |
| + break; |
| + case SessionErrorIncompatibleProtocol: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorIncompatibleProtocol."]; |
| + break; |
| + case SessionErrorAuthenticationFailed: |
| + message = [MDCSnackbarMessage messageWithText:@"Error: Invalid Pin."]; |
| + [_pinEntryView clearPinEntry]; |
| + break; |
| + case SessionErrorInvalidAccount: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorInvalidAccount."]; |
| + break; |
| + case SessionErrorChannelConnectionError: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorChannelConnectionError."]; |
| + break; |
| + case SessionErrorSignalingError: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorSignalingError."]; |
| + break; |
| + case SessionErrorSignalingTimeout: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorSignalingTimeout."]; |
| + break; |
| + case SessionErrorHostOverload: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorHostOverload."]; |
| + break; |
| + case SessionErrorMaxSessionLength: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorMaxSessionLength."]; |
| + break; |
| + case SessionErrorHostConfigurationError: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorHostConfigurationError."]; |
| + break; |
| + case SessionErrorUnknownError: |
| + message = [MDCSnackbarMessage |
| + messageWithText:@"Error: SessionErrorUnknownError."]; |
| + break; |
| + } |
| + if (message.text) { |
| + [MDCSnackbarManager showMessage:message]; |
| + } |
| +} |
| + |
| - (void)didProvidePin:(NSString*)pin createPairing:(BOOL)createPairing { |
| // TODO(nicholss): There is an open question if createPairing is supported on |
| // iOS. Need to fingure this out. |
| @@ -347,7 +534,8 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| state = ClientViewConnected; |
| break; |
| case SessionFailed: |
| - // TODO(nicholss): Implement an error screen. |
| + state = ClientViewError; |
| + break; |
| case SessionClosed: |
| state = ClientViewClosed; |
| break; |
| @@ -355,6 +543,7 @@ static const CGFloat kKeyboardAnimationTime = 0.3; |
| LOG(ERROR) << "Unknown State for Session, " << sessionDetails.state; |
| return; |
| } |
| + _lastError = sessionDetails.error; |
| [[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
| [self setState:state]; |
| }]; |