Commit 88505a5e authored by Yuwei Huang's avatar Yuwei Huang Committed by Commit Bot

[CRD iOS] Add a view for host fetch failure for non-user-triggered refresh

Previously if a host list fetch is not triggered by the user (refresh
control) and fails, then the app will simply pop up a toast complaining
about unavailable network and show nothing else. Pull-to-refresh
doesn't work.

This CL fixes this by providing a dialog-like view to allow user to
retry in that case.

Screenshot:
https://drive.google.com/file/d/0BytzIZKeM8nBMmN3b3A0bUhoMFU/view?usp=sharing

Bug: 771532
Change-Id: I64cb4015acc066322c3c6127b79b87d13cb06652
Reviewed-on: https://chromium-review.googlesource.com/700316Reviewed-by: default avatarScott Nichols <nicholss@chromium.org>
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
Cr-Commit-Position: refs/heads/master@{#506537}
parent c6afa6fb
...@@ -36,6 +36,8 @@ source_set("common_source_set") { ...@@ -36,6 +36,8 @@ source_set("common_source_set") {
"host_collection_view_cell.mm", "host_collection_view_cell.mm",
"host_collection_view_controller.h", "host_collection_view_controller.h",
"host_collection_view_controller.mm", "host_collection_view_controller.mm",
"host_fetching_error_view_controller.h",
"host_fetching_error_view_controller.mm",
"host_fetching_view_controller.h", "host_fetching_view_controller.h",
"host_fetching_view_controller.mm", "host_fetching_view_controller.mm",
"host_setup_footer_view.h", "host_setup_footer_view.h",
......
// 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.
#ifndef REMOTING_IOS_APP_HOST_FETCHING_ERROR_VIEW_CONTROLLER_H_
#define REMOTING_IOS_APP_HOST_FETCHING_ERROR_VIEW_CONTROLLER_H_
#import <UIKit/UIKit.h>
// This VC shows a dialog-like view to allow the user to retry host list
// fetching. This is used when the host list has never been successfully,
// fetched, i.e. pull-to-refresh is not available.
@interface HostFetchingErrorViewController : UIViewController
// Called when the retry button is tapped.
@property(nonatomic) void (^onRetryCallback)();
// Returns the label that shows the error message.
@property(nonatomic, readonly) UILabel* label;
@end
#endif // REMOTING_IOS_APP_HOST_FETCHING_ERROR_VIEW_CONTROLLER_H_
// 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.
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#import "remoting/ios/app/host_fetching_error_view_controller.h"
#import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MDCTypography.h"
#import "remoting/ios/app/remoting_theme.h"
#include "remoting/base/string_resources.h"
#include "ui/base/l10n/l10n_util.h"
static const CGFloat kPadding = 20;
static const CGFloat kLineSpace = 30;
// The actual width will be min(kMaxWidth, screen width)
static const CGFloat kMaxWidth = 500;
// Adjust for the padding already existed inside the button.
static const CGFloat kButtonRightPaddingAdjustment = -10;
static const CGFloat kButtonBottomPaddingAdjustment = -5;
@implementation HostFetchingErrorViewController {
UILabel* _label;
}
@synthesize onRetryCallback = _onRetryCallback;
- (instancetype)init {
if (self = [super init]) {
// Label should be created right under init because it may be accessed
// before the view is loaded.
_label = [[UILabel alloc] initWithFrame:CGRectZero];
}
return self;
}
- (void)viewDidLoad {
UIView* contentView = [[UIView alloc] initWithFrame:CGRectZero];
contentView.backgroundColor = RemotingTheme.setupListBackgroundColor;
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:contentView];
_label.font = MDCTypography.body1Font;
_label.numberOfLines = 0;
_label.lineBreakMode = NSLineBreakByWordWrapping;
_label.textColor = RemotingTheme.setupListTextColor;
_label.translatesAutoresizingMaskIntoConstraints = NO;
[contentView addSubview:_label];
MDCButton* button = [[MDCButton alloc] initWithFrame:CGRectZero];
[button setTitle:l10n_util::GetNSString(IDS_RETRY)
forState:UIControlStateNormal];
[button setBackgroundColor:UIColor.clearColor forState:UIControlStateNormal];
[button setTitleColor:RemotingTheme.flatButtonTextColor
forState:UIControlStateNormal];
[button sizeToFit];
[button addTarget:self
action:@selector(didTapRetry:)
forControlEvents:UIControlEventTouchUpInside];
button.translatesAutoresizingMaskIntoConstraints = NO;
[contentView addSubview:button];
NSLayoutConstraint* maxWidthConstraint =
[contentView.widthAnchor constraintEqualToConstant:kMaxWidth];
maxWidthConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
maxWidthConstraint,
// Trumps |maxWidthConstraint| when necessary.
[contentView.widthAnchor
constraintLessThanOrEqualToAnchor:self.view.widthAnchor],
[contentView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[contentView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
[_label.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor
constant:kPadding],
[_label.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor
constant:-kPadding],
[_label.topAnchor constraintEqualToAnchor:contentView.topAnchor
constant:kPadding],
[button.trailingAnchor
constraintEqualToAnchor:contentView.trailingAnchor
constant:-kPadding - kButtonRightPaddingAdjustment],
[button.topAnchor constraintEqualToAnchor:_label.bottomAnchor
constant:kLineSpace],
[button.bottomAnchor
constraintEqualToAnchor:contentView.bottomAnchor
constant:-kPadding - kButtonBottomPaddingAdjustment],
]];
}
- (UILabel*)label {
return _label;
}
#pragma mark - Private
- (void)didTapRetry:(id)button {
if (_onRetryCallback) {
_onRetryCallback();
}
}
@end
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
#import "remoting/ios/app/app_delegate.h" #import "remoting/ios/app/app_delegate.h"
#import "remoting/ios/app/client_connection_view_controller.h" #import "remoting/ios/app/client_connection_view_controller.h"
#import "remoting/ios/app/host_collection_view_controller.h" #import "remoting/ios/app/host_collection_view_controller.h"
#import "remoting/ios/app/host_fetching_error_view_controller.h"
#import "remoting/ios/app/host_fetching_view_controller.h" #import "remoting/ios/app/host_fetching_view_controller.h"
#import "remoting/ios/app/host_setup_view_controller.h" #import "remoting/ios/app/host_setup_view_controller.h"
#import "remoting/ios/app/host_view_controller.h" #import "remoting/ios/app/host_view_controller.h"
...@@ -88,6 +89,7 @@ ConnectionType GetConnectionType() { ...@@ -88,6 +89,7 @@ ConnectionType GetConnectionType() {
MDCAppBar* _appBar; MDCAppBar* _appBar;
HostCollectionViewController* _collectionViewController; HostCollectionViewController* _collectionViewController;
HostFetchingViewController* _fetchingViewController; HostFetchingViewController* _fetchingViewController;
HostFetchingErrorViewController* _fetchingErrorViewController;
HostSetupViewController* _setupViewController; HostSetupViewController* _setupViewController;
RemotingService* _remotingService; RemotingService* _remotingService;
...@@ -108,6 +110,11 @@ ConnectionType GetConnectionType() { ...@@ -108,6 +110,11 @@ ConnectionType GetConnectionType() {
if (self) { if (self) {
_remotingService = RemotingService.instance; _remotingService = RemotingService.instance;
__weak RemotingViewController* weakSelf = self;
RemotingRefreshAction refreshAction = ^{
[weakSelf didSelectRefresh];
};
_collectionViewController = [[HostCollectionViewController alloc] _collectionViewController = [[HostCollectionViewController alloc]
initWithCollectionViewLayout:layout]; initWithCollectionViewLayout:layout];
_collectionViewController.delegate = self; _collectionViewController.delegate = self;
...@@ -115,6 +122,10 @@ ConnectionType GetConnectionType() { ...@@ -115,6 +122,10 @@ ConnectionType GetConnectionType() {
_fetchingViewController = [[HostFetchingViewController alloc] init]; _fetchingViewController = [[HostFetchingViewController alloc] init];
_fetchingErrorViewController =
[[HostFetchingErrorViewController alloc] init];
_fetchingErrorViewController.onRetryCallback = refreshAction;
_setupViewController = [[HostSetupViewController alloc] init]; _setupViewController = [[HostSetupViewController alloc] init];
_setupViewController.scrollViewDelegate = self.headerViewController; _setupViewController.scrollViewDelegate = self.headerViewController;
...@@ -151,10 +162,6 @@ ConnectionType GetConnectionType() { ...@@ -151,10 +162,6 @@ ConnectionType GetConnectionType() {
[(MDCShadowLayer*)layer setElevation:elevation]; [(MDCShadowLayer*)layer setElevation:elevation];
}]; }];
__weak RemotingViewController* weakSelf = self;
RemotingRefreshAction refreshAction = ^{
[weakSelf didSelectRefresh];
};
_refreshControls = @[ _refreshControls = @[
[[RefreshControlProvider instance] [[RefreshControlProvider instance]
createForScrollView:_collectionViewController.collectionView createForScrollView:_collectionViewController.collectionView
...@@ -199,7 +206,7 @@ ConnectionType GetConnectionType() { ...@@ -199,7 +206,7 @@ ConnectionType GetConnectionType() {
[NSNotificationCenter.defaultCenter [NSNotificationCenter.defaultCenter
addObserver:self addObserver:self
selector:@selector(hostListFetchDidFailedNotification:) selector:@selector(hostListFetchDidFailNotification:)
name:kHostListFetchDidFail name:kHostListFetchDidFail
object:nil]; object:nil];
} }
...@@ -222,25 +229,10 @@ ConnectionType GetConnectionType() { ...@@ -222,25 +229,10 @@ ConnectionType GetConnectionType() {
[self refreshContent]; [self refreshContent];
} }
- (void)hostListFetchDidFailedNotification:(NSNotification*)notification { - (void)hostListFetchDidFailNotification:(NSNotification*)notification {
HostListFetchFailureReason reason = (HostListFetchFailureReason) HostListFetchFailureReason reason = (HostListFetchFailureReason)
[notification.userInfo[kHostListFetchFailureReasonKey] integerValue]; [notification.userInfo[kHostListFetchFailureReasonKey] integerValue];
int messageId; [self handleHostListFetchFailure:reason];
switch (reason) {
case HostListFetchFailureReasonNetworkError:
messageId = IDS_ERROR_NETWORK_ERROR;
break;
case HostListFetchFailureReasonAuthError:
messageId = IDS_ERROR_OAUTH_TOKEN_INVALID;
break;
default:
NOTREACHED();
return;
}
[MDCSnackbarManager
showMessage:[MDCSnackbarMessage
messageWithText:l10n_util::GetNSString(messageId)]];
[self stopAllRefreshControls];
} }
#pragma mark - HostCollectionViewControllerDelegate #pragma mark - HostCollectionViewControllerDelegate
...@@ -351,7 +343,14 @@ animationControllerForDismissedController:(UIViewController*)dismissed { ...@@ -351,7 +343,14 @@ animationControllerForDismissedController:(UIViewController*)dismissed {
} }
if (_remotingService.hostListState == HostListStateNotFetched) { if (_remotingService.hostListState == HostListStateNotFetched) {
self.contentViewController = nil; if (_remotingService.lastFetchFailureReason ==
HostListFetchFailureReasonNoFailure) {
self.contentViewController = nil;
} else {
// hostListFetchDidFailNotification might miss the first failure happened
// before the notification is registered. This logic covers that.
[self handleHostListFetchFailure:_remotingService.lastFetchFailureReason];
}
return; return;
} }
...@@ -381,6 +380,42 @@ animationControllerForDismissedController:(UIViewController*)dismissed { ...@@ -381,6 +380,42 @@ animationControllerForDismissedController:(UIViewController*)dismissed {
} }
} }
- (void)handleHostListFetchFailure:(HostListFetchFailureReason)reason {
int messageId;
switch (reason) {
case HostListFetchFailureReasonNetworkError:
messageId = IDS_ERROR_NETWORK_ERROR;
break;
case HostListFetchFailureReasonAuthError:
messageId = IDS_ERROR_OAUTH_TOKEN_INVALID;
break;
default:
NOTREACHED();
return;
}
NSString* errorText = l10n_util::GetNSString(messageId);
if ([self isAnyRefreshControlRefreshing]) {
// User could just try pull-to-refresh again to refresh. We just need to
// show the error as a toast.
[MDCSnackbarManager
showMessage:[MDCSnackbarMessage messageWithText:errorText]];
[self stopAllRefreshControls];
return;
}
// Pull-to-refresh is not available. We need to show a dedicated view to allow
// user to retry.
// Dismiss snackbars and hide the SSO menu so that the accessibility focus
// can shift into the label.
[MDCSnackbarManager dismissAndCallCompletionBlocksWithCategory:nil];
[AppDelegate.instance hideMenuAnimated:YES];
_fetchingErrorViewController.label.text = errorText;
remoting::SetAccessibilityFocusElement(_fetchingErrorViewController.label);
self.contentViewController = _fetchingErrorViewController;
}
- (BOOL)isAnyRefreshControlRefreshing { - (BOOL)isAnyRefreshControlRefreshing {
for (id<RemotingRefreshControl> control in _refreshControls) { for (id<RemotingRefreshControl> control in _refreshControls) {
if (control.isRefreshing) { if (control.isRefreshing) {
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
@protocol RemotingAuthentication; @protocol RemotingAuthentication;
typedef NS_ENUM(NSInteger, HostListState) { typedef NS_ENUM(NSInteger, HostListState) {
// Nobody has requested a host list fetch. // Nobody has requested a host list fetch since login or last failure.
HostListStateNotFetched, HostListStateNotFetched,
// The host list is currently being fetched. // The host list is currently being fetched.
...@@ -24,6 +24,7 @@ typedef NS_ENUM(NSInteger, HostListState) { ...@@ -24,6 +24,7 @@ typedef NS_ENUM(NSInteger, HostListState) {
}; };
typedef NS_ENUM(NSInteger, HostListFetchFailureReason) { typedef NS_ENUM(NSInteger, HostListFetchFailureReason) {
HostListFetchFailureReasonNoFailure,
HostListFetchFailureReasonNetworkError, HostListFetchFailureReasonNetworkError,
HostListFetchFailureReasonAuthError, HostListFetchFailureReasonAuthError,
HostListFetchFailureReasonUnknown, HostListFetchFailureReasonUnknown,
...@@ -64,6 +65,12 @@ extern NSString* const kUserInfo; ...@@ -64,6 +65,12 @@ extern NSString* const kUserInfo;
// resources used by the Chromoting clients // resources used by the Chromoting clients
@property(nonatomic, readonly) remoting::ChromotingClientRuntime* runtime; @property(nonatomic, readonly) remoting::ChromotingClientRuntime* runtime;
// Returns the last failure reason when fetching the host list. Returns
// HostListFetchFailureReasonNoFailure when the host list has never been fetched
// or the last fetch has succeeded.
@property(nonatomic, readonly)
HostListFetchFailureReason lastFetchFailureReason;
// This must be set immediately after the authentication object is created. It // This must be set immediately after the authentication object is created. It
// can only be set once. // can only be set once.
@property(nonatomic) id<RemotingAuthentication> authentication; @property(nonatomic) id<RemotingAuthentication> authentication;
......
...@@ -54,6 +54,7 @@ NSString* const kUserInfo = @"kUserInfo"; ...@@ -54,6 +54,7 @@ NSString* const kUserInfo = @"kUserInfo";
@synthesize hosts = _hosts; @synthesize hosts = _hosts;
@synthesize hostListState = _hostListState; @synthesize hostListState = _hostListState;
@synthesize lastFetchFailureReason = _lastFetchFailureReason;
// RemotingService is a singleton. // RemotingService is a singleton.
+ (RemotingService*)instance { + (RemotingService*)instance {
...@@ -71,6 +72,7 @@ NSString* const kUserInfo = @"kUserInfo"; ...@@ -71,6 +72,7 @@ NSString* const kUserInfo = @"kUserInfo";
_hosts = nil; _hosts = nil;
// TODO(yuweih): Maybe better to just cancel the previous request. // TODO(yuweih): Maybe better to just cancel the previous request.
_hostListState = HostListStateNotFetched; _hostListState = HostListStateNotFetched;
_lastFetchFailureReason = HostListFetchFailureReasonNoFailure;
// TODO(nicholss): This might need a pointer back to the service. // TODO(nicholss): This might need a pointer back to the service.
_clientRuntimeDelegate = _clientRuntimeDelegate =
new remoting::IosClientRuntimeDelegate(); new remoting::IosClientRuntimeDelegate();
...@@ -152,6 +154,9 @@ NSString* const kUserInfo = @"kUserInfo"; ...@@ -152,6 +154,9 @@ NSString* const kUserInfo = @"kUserInfo";
if (state == _hostListState) { if (state == _hostListState) {
return; return;
} }
if (state == HostListStateFetching || state == HostListStateFetched) {
_lastFetchFailureReason = HostListFetchFailureReasonNoFailure;
}
_hostListState = state; _hostListState = state;
[[NSNotificationCenter defaultCenter] [[NSNotificationCenter defaultCenter]
postNotificationName:kHostListStateDidChange postNotificationName:kHostListStateDidChange
...@@ -208,22 +213,22 @@ NSString* const kUserInfo = @"kUserInfo"; ...@@ -208,22 +213,22 @@ NSString* const kUserInfo = @"kUserInfo";
return; return;
} }
HostListFetchFailureReason reason;
switch (status) { switch (status) {
case RemotingAuthenticationStatusNetworkError: case RemotingAuthenticationStatusNetworkError:
reason = HostListFetchFailureReasonNetworkError; _lastFetchFailureReason = HostListFetchFailureReasonNetworkError;
break; break;
case RemotingAuthenticationStatusAuthError: case RemotingAuthenticationStatusAuthError:
reason = HostListFetchFailureReasonAuthError; _lastFetchFailureReason = HostListFetchFailureReasonAuthError;
break; break;
default: default:
reason = HostListFetchFailureReasonUnknown; _lastFetchFailureReason = HostListFetchFailureReasonUnknown;
} }
[NSNotificationCenter.defaultCenter [NSNotificationCenter.defaultCenter
postNotificationName:kHostListFetchDidFail postNotificationName:kHostListFetchDidFail
object:self object:self
userInfo:@{ userInfo:@{
kHostListFetchFailureReasonKey : @(reason) kHostListFetchFailureReasonKey :
@(_lastFetchFailureReason)
}]; }];
}]; }];
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment