123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- // 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/remoting_view_controller.h"
- #include <SystemConfiguration/SystemConfiguration.h>
- #include <netinet/in.h>
- #import <MaterialComponents/MDCAppBarViewController.h>
- #import <MaterialComponents/MaterialAnimationTiming.h>
- #import <MaterialComponents/MaterialDialogs.h>
- #import <MaterialComponents/MaterialShadowElevations.h>
- #import <MaterialComponents/MaterialShadowLayer.h>
- #import <MaterialComponents/MaterialSnackbar.h>
- #import "base/bind.h"
- #include "base/mac/scoped_cftyperef.h"
- #include "base/strings/sys_string_conversions.h"
- #include "remoting/base/oauth_token_getter.h"
- #include "remoting/base/string_resources.h"
- #include "remoting/client/connect_to_host_info.h"
- #import "remoting/ios/app/account_manager.h"
- #import "remoting/ios/app/app_delegate.h"
- #import "remoting/ios/app/client_connection_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_setup_view_controller.h"
- #import "remoting/ios/app/host_view_controller.h"
- #import "remoting/ios/app/refresh_control_provider.h"
- #import "remoting/ios/app/remoting_theme.h"
- #import "remoting/ios/app/view_utils.h"
- #import "remoting/ios/domain/client_session_details.h"
- #include "remoting/ios/facade/host_list_service.h"
- #import "remoting/ios/facade/remoting_service.h"
- #include "ui/base/l10n/l10n_util.h"
- #if !defined(__has_feature) || !__has_feature(objc_arc)
- #error "This file requires ARC support."
- #endif
- static CGFloat kHostInset = 5.f;
- namespace {
- #pragma mark - Network Reachability
- enum class ConnectionType {
- UNKNOWN,
- NONE,
- WWAN,
- WIFI,
- };
- ConnectionType GetConnectionType() {
- // 0.0.0.0 is a special token that causes reachability to monitor the general
- // routing status of the device, both IPv4 and IPv6.
- struct sockaddr_in addr = {0};
- addr.sin_len = sizeof(addr);
- addr.sin_family = AF_INET;
- base::ScopedCFTypeRef<SCNetworkReachabilityRef> reachability(
- SCNetworkReachabilityCreateWithAddress(
- kCFAllocatorDefault, reinterpret_cast<struct sockaddr*>(&addr)));
- SCNetworkReachabilityFlags flags;
- BOOL success = SCNetworkReachabilityGetFlags(reachability, &flags);
- if (!success) {
- return ConnectionType::UNKNOWN;
- }
- BOOL isReachable = flags & kSCNetworkReachabilityFlagsReachable;
- BOOL needsConnection = flags & kSCNetworkReachabilityFlagsConnectionRequired;
- BOOL isNetworkReachable = isReachable && !needsConnection;
- if (!isNetworkReachable) {
- return ConnectionType::NONE;
- } else if (flags & kSCNetworkReachabilityFlagsIsWWAN) {
- return ConnectionType::WWAN;
- }
- return ConnectionType::WIFI;
- }
- } // namespace
- #pragma mark - RemotingViewController
- using remoting::HostListService;
- @interface RemotingViewController ()<HostCollectionViewControllerDelegate,
- UIViewControllerAnimatedTransitioning,
- UIViewControllerTransitioningDelegate> {
- MDCDialogTransitionController* _dialogTransitionController;
- MDCAppBarViewController* _appBarViewController;
- HostCollectionViewController* _collectionViewController;
- HostFetchingViewController* _fetchingViewController;
- HostFetchingErrorViewController* _fetchingErrorViewController;
- HostSetupViewController* _setupViewController;
- HostListService* _hostListService;
- base::CallbackListSubscription _hostListStateSubscription;
- base::CallbackListSubscription _hostListFetchFailureSubscription;
- NSArray<id<RemotingRefreshControl>>* _refreshControls;
- }
- @end
- @implementation RemotingViewController
- - (instancetype)init {
- UICollectionViewFlowLayout* layout =
- [[MDCCollectionViewFlowLayout alloc] init];
- layout.minimumInteritemSpacing = 0;
- CGFloat sectionInset = kHostInset * 2.f;
- [layout setSectionInset:UIEdgeInsetsMake(sectionInset, sectionInset,
- sectionInset, sectionInset)];
- self = [super init];
- if (self) {
- _hostListService = HostListService::GetInstance();
- __weak RemotingViewController* weakSelf = self;
- RemotingRefreshAction refreshAction = ^{
- [weakSelf didSelectRefresh];
- };
- _collectionViewController = [[HostCollectionViewController alloc]
- initWithCollectionViewLayout:layout];
- _collectionViewController.delegate = self;
- _collectionViewController.scrollViewDelegate = self.headerViewController;
- _fetchingViewController = [[HostFetchingViewController alloc] init];
- _fetchingErrorViewController =
- [[HostFetchingErrorViewController alloc] init];
- _fetchingErrorViewController.onRetryCallback = refreshAction;
- _setupViewController = [[HostSetupViewController alloc] init];
- _setupViewController.scrollViewDelegate = self.headerViewController;
- _appBarViewController = [[MDCAppBarViewController alloc] init];
- [self addChildViewController:_appBarViewController];
- self.navigationItem.title =
- l10n_util::GetNSString(IDS_PRODUCT_NAME).lowercaseString;
- [self.navigationItem setHidesBackButton:YES animated:NO];
- _appBarViewController.headerView.backgroundColor =
- RemotingTheme.hostListBackgroundColor;
- _appBarViewController.navigationBar.backgroundColor =
- RemotingTheme.hostListBackgroundColor;
- MDCNavigationBarTextColorAccessibilityMutator* mutator =
- [[MDCNavigationBarTextColorAccessibilityMutator alloc] init];
- [mutator mutate:_appBarViewController.navigationBar];
- MDCFlexibleHeaderView* headerView = self.headerViewController.headerView;
- headerView.backgroundColor = [UIColor clearColor];
- // Use a custom shadow under the flexible header.
- MDCShadowLayer* shadowLayer = [MDCShadowLayer layer];
- [headerView setShadowLayer:shadowLayer
- intensityDidChangeBlock:^(CALayer* layer, CGFloat intensity) {
- CGFloat elevation = MDCShadowElevationAppBar * intensity;
- [(MDCShadowLayer*)layer setElevation:elevation];
- }];
- _refreshControls = @[
- [[RefreshControlProvider instance]
- createForScrollView:_collectionViewController.collectionView
- actionBlock:refreshAction],
- [[RefreshControlProvider instance]
- createForScrollView:_setupViewController.tableView
- actionBlock:refreshAction],
- ];
- }
- return self;
- }
- - (void)dealloc {
- [NSNotificationCenter.defaultCenter removeObserver:self];
- }
- #pragma mark - UIViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- UIImage* image = [UIImage imageNamed:@"Background"];
- UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
- [self.view addSubview:imageView];
- [self.view sendSubviewToBack:imageView];
- imageView.translatesAutoresizingMaskIntoConstraints = NO;
- [self.view addSubview:_appBarViewController.view];
- [_appBarViewController didMoveToParentViewController:self];
- UIViewController* accountParticleDiscViewController =
- remoting::ios::AccountManager::GetInstance()
- ->CreateAccountParticleDiscViewController();
- accountParticleDiscViewController.view
- .translatesAutoresizingMaskIntoConstraints = NO;
- [self addChildViewController:accountParticleDiscViewController];
- [self.view addSubview:accountParticleDiscViewController.view];
- [accountParticleDiscViewController didMoveToParentViewController:self];
- [NSLayoutConstraint activateConstraints:@[
- [[imageView widthAnchor]
- constraintGreaterThanOrEqualToAnchor:[self.view widthAnchor]],
- [[imageView heightAnchor]
- constraintGreaterThanOrEqualToAnchor:[self.view heightAnchor]],
- [accountParticleDiscViewController.view.topAnchor
- constraintEqualToAnchor:_appBarViewController.navigationBar.topAnchor],
- [accountParticleDiscViewController.view.trailingAnchor
- constraintEqualToAnchor:_appBarViewController.navigationBar
- .trailingAnchor],
- [accountParticleDiscViewController.view.widthAnchor
- constraintEqualToConstant:accountParticleDiscViewController
- .preferredContentSize.width],
- [accountParticleDiscViewController.view.heightAnchor
- constraintEqualToConstant:accountParticleDiscViewController
- .preferredContentSize.height],
- ]];
- __weak __typeof(self) weakSelf = self;
- _hostListStateSubscription =
- _hostListService->RegisterHostListStateCallback(base::BindRepeating(^{
- [weakSelf hostListStateDidChange];
- }));
- _hostListFetchFailureSubscription =
- _hostListService->RegisterFetchFailureCallback(base::BindRepeating(^{
- [weakSelf hostListFetchDidFail];
- }));
- }
- - (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
- // Just in case the view controller misses the host list state event before
- // the listener is registered.
- [self refreshContent];
- _hostListService->RequestFetch();
- [NSNotificationCenter.defaultCenter
- addObserver:self
- selector:@selector(applicationDidBecomeActive:)
- name:UIApplicationDidBecomeActiveNotification
- object:nil];
- }
- - (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [NSNotificationCenter.defaultCenter
- removeObserver:self
- name:UIApplicationDidBecomeActiveNotification
- object:nil];
- }
- - (UIStatusBarStyle)preferredStatusBarStyle {
- return UIStatusBarStyleLightContent;
- }
- #pragma mark - HostListService Callbacks
- - (void)hostListStateDidChange {
- [self refreshContent];
- }
- - (void)hostListFetchDidFail {
- [self handleHostListFetchFailure];
- }
- #pragma mark - HostCollectionViewControllerDelegate
- - (void)didSelectCell:(HostCollectionViewCell*)cell
- completion:(void (^)())completionBlock {
- if (![cell.hostInfo isOnline]) {
- MDCSnackbarMessage* message = [[MDCSnackbarMessage alloc] init];
- message.text = l10n_util::GetNSString(IDS_HOST_OFFLINE_TOOLTIP);
- [MDCSnackbarManager.defaultManager showMessage:message];
- return;
- }
- if (GetConnectionType() == ConnectionType::NONE) {
- [MDCSnackbarManager.defaultManager
- showMessage:[MDCSnackbarMessage
- messageWithText:l10n_util::GetNSString(
- IDS_ERROR_NETWORK_ERROR)]];
- return;
- }
- [MDCSnackbarManager.defaultManager
- dismissAndCallCompletionBlocksWithCategory:nil];
- ClientConnectionViewController* clientConnectionViewController =
- [[ClientConnectionViewController alloc] initWithHostInfo:cell.hostInfo];
- [self.navigationController pushViewController:clientConnectionViewController
- animated:YES];
- completionBlock();
- }
- - (NSInteger)getHostCount {
- return _hostListService->hosts().size();
- }
- - (HostInfo*)getHostAtIndexPath:(NSIndexPath*)path {
- return [[HostInfo alloc]
- initWithRemotingHostInfo:_hostListService->hosts()[path.row]];
- }
- #pragma mark - UIViewControllerTransitioningDelegate
- - (nullable id<UIViewControllerAnimatedTransitioning>)
- animationControllerForDismissedController:(UIViewController*)dismissed {
- return self;
- }
- #pragma mark - UIViewControllerAnimatedTransitioning
- - (void)animateTransition:
- (id<UIViewControllerContextTransitioning>)transitionContext {
- }
- - (NSTimeInterval)transitionDuration:
- (id<UIViewControllerContextTransitioning>)transitionContext {
- return 0.2;
- }
- #pragma mark - Private
- - (void)didSelectRefresh {
- _hostListService->RequestFetch();
- }
- - (void)refreshContent {
- if (_hostListService->state() == HostListService::State::FETCHING) {
- // We don't need to show the fetching view when either the host list or the
- // setup view is already shown. Refresh control will handle the
- // user-triggered refresh, and we don't need to show anything if
- // that's a background refresh (e.g. user just closed the session).
- if (self.contentViewController != _collectionViewController &&
- self.contentViewController != _setupViewController) {
- self.contentViewController = _fetchingViewController;
- }
- return;
- }
- if (_hostListService->state() == HostListService::State::NOT_FETCHED) {
- if (!_hostListService->last_fetch_failure()) {
- self.contentViewController = nil;
- } else {
- // hostListFetchDidFailNotification might miss the first failure happened
- // before the notification is registered. This logic covers that.
- [self handleHostListFetchFailure];
- }
- return;
- }
- DCHECK(_hostListService->state() == HostListService::State::FETCHED);
- [self stopAllRefreshControls];
- if (_hostListService->hosts().size() > 0) {
- [_collectionViewController.collectionView reloadData];
- self.headerViewController.headerView.trackingScrollView =
- _collectionViewController.collectionView;
- self.contentViewController = _collectionViewController;
- } else {
- self.headerViewController.headerView.trackingScrollView =
- _setupViewController.tableView;
- self.contentViewController = _setupViewController;
- }
- self.contentViewController.view.frame = self.view.bounds;
- }
- - (void)handleHostListFetchFailure {
- const auto* failure = _hostListService->last_fetch_failure();
- if (!failure) {
- return;
- }
- NSString* errorText = base::SysUTF8ToNSString(failure->localized_description);
- if ([self isAnyRefreshControlRefreshing]) {
- // User could just try pull-to-refresh again to refresh. We just need to
- // show the error as a toast.
- [MDCSnackbarManager.defaultManager
- 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 so that the accessibility focus can shift into the
- // label.
- // TODO(yuweih): See if we really need to hide the account menu in this case,
- // since it requires nontrivial changes.
- [MDCSnackbarManager.defaultManager
- dismissAndCallCompletionBlocksWithCategory:nil];
- _fetchingErrorViewController.label.text = errorText;
- remoting::SetAccessibilityFocusElement(_fetchingErrorViewController.label);
- self.contentViewController = _fetchingErrorViewController;
- }
- - (BOOL)isAnyRefreshControlRefreshing {
- for (id<RemotingRefreshControl> control in _refreshControls) {
- if (control.isRefreshing) {
- return YES;
- }
- }
- return NO;
- }
- - (void)stopAllRefreshControls {
- for (id<RemotingRefreshControl> control in _refreshControls) {
- [control endRefreshing];
- }
- }
- - (void)applicationDidBecomeActive:(UIApplication*)application {
- _hostListService->RequestFetch();
- }
- @end
|