123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- // Copyright 2017 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #import "remoting/ios/app/client_connection_view_controller.h"
- #import <MaterialComponents/MDCActivityIndicator.h>
- #import <MaterialComponents/MDCNavigationBar.h>
- #import <MaterialComponents/MaterialButtons.h>
- #import <MaterialComponents/MaterialCollections.h>
- #import <MaterialComponents/MaterialSnackbar.h>
- #include "base/logging.h"
- #include "base/strings/sys_string_conversions.h"
- #include "remoting/base/string_resources.h"
- #import "remoting/ios/app/help_and_feedback.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/app/view_utils.h"
- #import "remoting/ios/domain/client_session_details.h"
- #import "remoting/ios/domain/host_info.h"
- #import "remoting/ios/facade/remoting_authentication.h"
- #import "remoting/ios/facade/remoting_service.h"
- #import "remoting/ios/session/remoting_client.h"
- #include "remoting/protocol/client_authentication_config.h"
- #include "ui/base/l10n/l10n_util.h"
- #if !defined(__has_feature) || !__has_feature(objc_arc)
- #error "This file requires ARC support."
- #endif
- static const CGFloat kIconRadius = 30.f;
- static const CGFloat kActivityIndicatorStrokeWidth = 3.f;
- static const CGFloat kActivityIndicatorRadius = 33.f;
- static const CGFloat kPinEntryViewWidth = 240.f;
- static const CGFloat kPinEntryViewHeight = 90.f;
- static const CGFloat kReconnectViewWidth = 240.f;
- static const CGFloat kReconnectViewHeight = 90.f;
- static const CGFloat kPadding = 20.f;
- static const CGFloat kMargin = 20.f;
- static const CGFloat kKeyboardAnimationTime = 0.3;
- static NSString* const kConnectionErrorFeedbackContext =
- @"ConnectionErrorFeedbackContext";
- using EntryPoint = remoting::ChromotingEvent::SessionEntryPoint;
- @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;
- BOOL _hasViewAppeared;
- }
- @property(nonatomic, assign) SessionErrorCode lastError;
- @end
- @implementation ClientConnectionViewController
- @synthesize state = _state;
- @synthesize lastError = _lastError;
- - (instancetype)initWithHostInfo:(HostInfo*)hostInfo {
- self = [super init];
- if (self) {
- _hostInfo = hostInfo;
- _remoteHostName = hostInfo.hostName;
- _hasViewAppeared = NO;
- // TODO(yuweih): This logic may be reused by other views.
- UIButton* cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
- [cancelButton setTitle:l10n_util::GetNSString(IDS_CANCEL).uppercaseString
- forState:UIControlStateNormal];
- [cancelButton
- setImage:[RemotingTheme
- .backIcon imageFlippedForRightToLeftLayoutDirection]
- forState:UIControlStateNormal];
- [cancelButton addTarget:self
- action:@selector(didTapCancel:)
- forControlEvents:UIControlEventTouchUpInside];
- self.navigationItem.leftBarButtonItem =
- [[UIBarButtonItem alloc] initWithCustomView:cancelButton];
- _navBar = [[MDCNavigationBar alloc] initWithFrame:CGRectZero];
- [_navBar observeNavigationItem:self.navigationItem];
- [_navBar setBackgroundColor:RemotingTheme.connectionViewBackgroundColor];
- MDCNavigationBarTextColorAccessibilityMutator* mutator =
- [[MDCNavigationBarTextColorAccessibilityMutator alloc] init];
- [mutator mutate:_navBar];
- [self.view addSubview:_navBar];
- _navBar.translatesAutoresizingMaskIntoConstraints = NO;
- // Attach navBar to the top of the view.
- UILayoutGuide* layoutGuide =
- remoting::SafeAreaLayoutGuideForView(self.view);
- [NSLayoutConstraint activateConstraints:@[
- [_navBar.topAnchor constraintEqualToAnchor:layoutGuide.topAnchor],
- [_navBar.leadingAnchor constraintEqualToAnchor:layoutGuide.leadingAnchor],
- [_navBar.trailingAnchor
- constraintEqualToAnchor:layoutGuide.trailingAnchor],
- ]];
- }
- return self;
- }
- - (void)dealloc {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
- #pragma mark - UIViewController
- - (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 =
- @[ RemotingTheme.connectionViewForegroundColor ];
- _activityIndicator.translatesAutoresizingMaskIntoConstraints = NO;
- [self.view addSubview:_activityIndicator];
- _statusLabel = [[UILabel alloc] initWithFrame:CGRectZero];
- _statusLabel.numberOfLines = 1;
- _statusLabel.lineBreakMode = NSLineBreakByTruncatingTail;
- _statusLabel.textColor = RemotingTheme.connectionViewForegroundColor;
- _statusLabel.textAlignment = NSTextAlignmentCenter;
- _statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
- [self.view addSubview:_statusLabel];
- _iconView = [[UIImageView alloc] initWithFrame:CGRectZero];
- _iconView.contentMode = UIViewContentModeCenter;
- _iconView.alpha = 0.87f;
- _iconView.backgroundColor = RemotingTheme.hostOnlineColor;
- _iconView.layer.cornerRadius = kIconRadius;
- _iconView.layer.masksToBounds = YES;
- _iconView.image = RemotingTheme.desktopIcon;
- _iconView.translatesAutoresizingMaskIntoConstraints = NO;
- [self.view addSubview:_iconView];
- _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;
- _pinEntryView.translatesAutoresizingMaskIntoConstraints = NO;
- [self.view addSubview:_pinEntryView];
- _pinEntryView.delegate = self;
- [self
- initializeLayoutConstraintsWithViews:NSDictionaryOfVariableBindings(
- _activityIndicator, _statusLabel,
- _iconView, _reconnectView,
- _pinEntryView)];
- [[NSNotificationCenter defaultCenter]
- addObserver:self
- selector:@selector(hostSessionStatusChanged:)
- name:kHostSessionStatusChanged
- object:nil];
- [self attemptConnectionToHostWithEntryPoint:EntryPoint::CONNECT_BUTTON];
- // Although keyboard listeners are registered here, they won't work properly
- // if the keyboard shows/hides before the view appears.
- [[NSNotificationCenter defaultCenter]
- addObserver:self
- selector:@selector(keyboardWillShow:)
- name:UIKeyboardWillShowNotification
- object:nil];
- [[NSNotificationCenter defaultCenter]
- addObserver:self
- selector:@selector(keyboardWillHide:)
- name:UIKeyboardWillHideNotification
- object:nil];
- }
- - (void)initializeLayoutConstraintsWithViews:(NSDictionary*)views {
- // Metrics to use in visual format strings.
- NSDictionary* layoutMetrics = @{
- @"padding" : @(kPadding),
- @"margin" : @(kMargin),
- @"iconDiameter" : @(kIconRadius * 2),
- @"pinEntryViewWidth" : @(kPinEntryViewWidth),
- @"pinEntryViewHeight" : @(kPinEntryViewHeight),
- @"reconnectViewWidth" : @(kReconnectViewWidth),
- @"reconnectViewHeight" : @(kReconnectViewHeight),
- };
- [_activityIndicator sizeToFit];
- NSString* f;
- // Horizontal constraints:
- [self.view addConstraints:
- [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.bottomAnchor
- constraintEqualToAnchor:self.view.centerYAnchor];
- _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];
- }
- - (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
- [self.navigationController setNavigationBarHidden:YES animated:animated];
- }
- - (void)viewDidAppear:(BOOL)animated {
- [super viewDidAppear:animated];
- [_activityIndicator startAnimating];
- _hasViewAppeared = YES;
- self.state = _state;
- }
- - (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [_activityIndicator stopAnimating];
- }
- - (BOOL)prefersStatusBarHidden {
- return YES;
- }
- #pragma mark - Keyboard
- - (void)keyboardWillShow:(NSNotification*)notification {
- CGSize keyboardSize =
- [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey]
- CGRectValue]
- .size;
- 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:_activityIndicator.frame.origin.y + 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:^{
- [self.view layoutIfNeeded];
- }];
- }
- #pragma mark - Properties
- - (void)setState:(ClientConnectionViewState)state {
- _state = state;
- if (!_hasViewAppeared) {
- // Showing different state will re-layout the view, which will be broken if
- // the view is not shown yet.
- return;
- }
- switch (_state) {
- case ClientViewConnecting:
- [self showConnectingState];
- break;
- case ClientViewPinPrompt:
- [self showPinPromptState];
- break;
- case ClientViewConnected:
- [self showConnectedState];
- break;
- case ClientViewReconnect:
- [self showReconnect];
- break;
- case ClientViewClosed:
- [self.navigationController popToRootViewControllerAnimated:YES];
- break;
- case ClientViewError:
- [self showError];
- break;
- }
- }
- #pragma mark - SessionReconnectViewDelegate
- - (void)didTapReconnect {
- [self attemptConnectionToHostWithEntryPoint:EntryPoint::RECONNECT_BUTTON];
- }
- - (void)didTapReport {
- [_client createFeedbackDataWithCallback:^(
- const remoting::FeedbackData& feedbackData) {
- [HelpAndFeedback.instance
- presentFeedbackFlowWithContext:kConnectionErrorFeedbackContext
- feedbackData:feedbackData];
- }];
- }
- #pragma mark - Private
- - (void)attemptConnectionToHostWithEntryPoint:(EntryPoint)entryPoint {
- _client = [[RemotingClient alloc] init];
- __weak ClientConnectionViewController* weakSelf = self;
- __weak RemotingClient* weakClient = _client;
- __weak HostInfo* weakHostInfo = _hostInfo;
- [RemotingService.instance.authentication
- callbackWithAccessToken:^(RemotingAuthenticationStatus status,
- NSString* userEmail, NSString* accessToken) {
- if (status == RemotingAuthenticationStatusSuccess) {
- [weakClient connectToHost:weakHostInfo
- username:userEmail
- accessToken:accessToken
- entryPoint:entryPoint];
- } else {
- LOG(ERROR) << "Failed to fetch access token for connectToHost. ("
- << status << ")";
- weakSelf.lastError = SessionErrorOAuthTokenInvalid;
- weakSelf.state = ClientViewError;
- }
- }];
- self.state = ClientViewConnecting;
- }
- - (void)showConnectingState {
- [_pinEntryView endEditing:YES];
- _statusLabel.text =
- [self stringWithHostNameForId:IDS_CONNECTING_TO_HOST_MESSAGE];
- [self focusOnStatusLabel];
- _pinEntryView.hidden = YES;
- _reconnectView.hidden = YES;
- _iconView.backgroundColor = RemotingTheme.hostOnlineColor;
- [_activityIndicator stopAnimating];
- _activityIndicator.cycleColors =
- @[ RemotingTheme.connectionViewForegroundColor ];
- _activityIndicator.indicatorMode = MDCActivityIndicatorModeIndeterminate;
- _activityIndicator.hidden = NO;
- [_activityIndicator startAnimating];
- }
- - (void)showPinPromptState {
- _statusLabel.text = [NSString stringWithFormat:@"%@", _remoteHostName];
- _iconView.backgroundColor = RemotingTheme.hostOnlineColor;
- [_activityIndicator stopAnimating];
- _activityIndicator.hidden = YES;
- _pinEntryView.hidden = NO;
- _reconnectView.hidden = YES;
- _reconnectView.hidden = YES;
- // TODO(yuweih): This may be called before viewDidAppear and miss the keyboard
- // callback.
- [_pinEntryView becomeFirstResponder];
- }
- - (void)showConnectedState {
- [_pinEntryView endEditing:YES];
- _statusLabel.text =
- [self stringWithHostNameForId:IDS_CONNECTED_TO_HOST_MESSAGE];
- [self focusOnStatusLabel];
- _pinEntryView.hidden = YES;
- [_pinEntryView clearPinEntry];
- _iconView.backgroundColor = RemotingTheme.hostOnlineColor;
- _activityIndicator.progress = 0.0;
- _activityIndicator.hidden = NO;
- _activityIndicator.indicatorMode = MDCActivityIndicatorModeDeterminate;
- _activityIndicator.cycleColors = @[ RemotingTheme.hostOnlineColor ];
- [_activityIndicator startAnimating];
- _activityIndicator.progress = 1.0;
- _reconnectView.hidden = YES;
- _reconnectView.hidden = YES;
- HostViewController* hostViewController =
- [[HostViewController alloc] initWithClient:_client];
- [self.navigationController pushViewController:hostViewController animated:NO];
- }
- // TODO(yuweih): Unused. Remove this method and the ClientViewReconnect enum.
- - (void)showReconnect {
- _statusLabel.text =
- [self stringWithHostNameForId:IDS_CONNECTION_CLOSED_FOR_HOST_MESSAGE];
- [self focusOnStatusLabel];
- _iconView.backgroundColor = RemotingTheme.hostErrorColor;
- [_activityIndicator stopAnimating];
- _activityIndicator.hidden = YES;
- _pinEntryView.hidden = YES;
- _reconnectView.hidden = NO;
- _reconnectView.errorText =
- l10n_util::GetNSString(IDS_MESSAGE_SESSION_FINISHED);
- [self.navigationController popToViewController:self animated:YES];
- }
- - (void)showError {
- // Error may happen after the session is connected. In this case we should
- // pop back to the client connection VC.
- if (self.navigationController.topViewController != self) {
- [self.navigationController popToViewController:self animated:YES];
- }
- _statusLabel.text =
- [self stringWithHostNameForId:IDS_ERROR_CONNECTING_TO_HOST_MESSAGE];
- _pinEntryView.hidden = YES;
- _iconView.backgroundColor = RemotingTheme.hostErrorColor;
- _activityIndicator.hidden = YES;
- NSString* message = nil;
- switch (_lastError) {
- case SessionErrorOk:
- // Do nothing.
- break;
- case SessionErrorPeerIsOffline:
- message = l10n_util::GetNSString(IDS_ERROR_HOST_IS_OFFLINE);
- break;
- case SessionErrorSessionRejected:
- message = l10n_util::GetNSString(IDS_ERROR_INVALID_ACCOUNT);
- break;
- case SessionErrorIncompatibleProtocol:
- message = l10n_util::GetNSString(IDS_ERROR_INCOMPATIBLE_PROTOCOL);
- break;
- case SessionErrorAuthenticationFailed:
- message = l10n_util::GetNSString(IDS_ERROR_INVALID_ACCESS_CODE);
- [_pinEntryView clearPinEntry];
- break;
- case SessionErrorInvalidAccount:
- message = l10n_util::GetNSString(IDS_ERROR_INVALID_ACCOUNT);
- break;
- case SessionErrorChannelConnectionError:
- message = l10n_util::GetNSString(IDS_ERROR_NETWORK_FAILURE);
- break;
- case SessionErrorSignalingError:
- message = l10n_util::GetNSString(IDS_ERROR_P2P_FAILURE);
- break;
- case SessionErrorSignalingTimeout:
- message = l10n_util::GetNSString(IDS_ERROR_HOST_IS_OFFLINE);
- break;
- case SessionErrorHostOverload:
- message = l10n_util::GetNSString(IDS_ERROR_HOST_OVERLOAD);
- break;
- case SessionErrorMaxSessionLength:
- message = l10n_util::GetNSString(IDS_ERROR_MAX_SESSION_LENGTH);
- break;
- case SessionErrorHostConfigurationError:
- message = l10n_util::GetNSString(IDS_ERROR_HOST_CONFIGURATION_ERROR);
- break;
- case SessionErrorUnknownError:
- message = l10n_util::GetNSString(IDS_ERROR_UNEXPECTED);
- break;
- case SessionErrorOAuthTokenInvalid:
- message = l10n_util::GetNSString(IDS_ERROR_OAUTH_TOKEN_INVALID);
- break;
- case SessionErrorThirdPartyAuthNotSupported:
- message = l10n_util::GetNSString(IDS_THIRD_PARTY_AUTH_NOT_SUPPORTED);
- break;
- }
- if (message) {
- _reconnectView.errorText = message;
- }
- _reconnectView.hidden = NO;
- remoting::SetAccessibilityFocusElement(_reconnectView);
- }
- - (void)didProvidePin:(NSString*)pin createPairing:(BOOL)createPairing {
- [[NSNotificationCenter defaultCenter]
- postNotificationName:kHostSessionPinProvided
- object:self
- userInfo:@{
- kHostSessionHostName : _remoteHostName,
- kHostSessionPin : pin,
- kHostSessionCreatePairing : @(createPairing)
- }];
- }
- - (void)didTapCancel:(id)sender {
- _client = nil;
- [self.navigationController popViewControllerAnimated:YES];
- }
- - (void)hostSessionStatusChanged:(NSNotification*)notification {
- NSLog(@"hostSessionStatusChanged: %@", [notification userInfo]);
- ClientConnectionViewState state;
- ClientSessionDetails* sessionDetails =
- [[notification userInfo] objectForKey:kSessionDetails];
- switch (sessionDetails.state) {
- case SessionInitializing:
- // Same as HostConnecting in UI. Fall-though.
- case SessionAuthenticated:
- // Same as HostConnecting in UI. Fall-though.
- case SessionConnecting:
- state = ClientViewConnecting;
- break;
- case SessionPinPrompt:
- _pinEntryView.supportsPairing = [[[notification userInfo]
- objectForKey:kSessionSupportsPairing] boolValue];
- state = ClientViewPinPrompt;
- break;
- case SessionConnected:
- state = ClientViewConnected;
- break;
- case SessionFailed:
- state = ClientViewError;
- break;
- case SessionClosed:
- // If the session is closed by the host, just go back to the host list and
- // show a toast.
- state = ClientViewClosed;
- [MDCSnackbarManager.defaultManager
- showMessage:[MDCSnackbarMessage
- messageWithText:l10n_util::GetNSString(
- IDS_MESSAGE_SESSION_FINISHED)]];
- break;
- default:
- LOG(ERROR) << "Unknown State for Session, " << sessionDetails.state;
- return;
- }
- _lastError = sessionDetails.error;
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- self.state = state;
- }];
- }
- - (NSString*)stringWithHostNameForId:(int)messageId {
- return l10n_util::GetNSStringF(messageId,
- base::SysNSStringToUTF16(_remoteHostName));
- }
- - (void)focusOnStatusLabel {
- remoting::SetAccessibilityFocusElement(_statusLabel);
- }
- @end
|