remoting_view_controller.mm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. // Copyright 2017 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #import "remoting/ios/app/remoting_view_controller.h"
  5. #include <SystemConfiguration/SystemConfiguration.h>
  6. #include <netinet/in.h>
  7. #import <MaterialComponents/MDCAppBarViewController.h>
  8. #import <MaterialComponents/MaterialAnimationTiming.h>
  9. #import <MaterialComponents/MaterialDialogs.h>
  10. #import <MaterialComponents/MaterialShadowElevations.h>
  11. #import <MaterialComponents/MaterialShadowLayer.h>
  12. #import <MaterialComponents/MaterialSnackbar.h>
  13. #import "base/bind.h"
  14. #include "base/mac/scoped_cftyperef.h"
  15. #include "base/strings/sys_string_conversions.h"
  16. #include "remoting/base/oauth_token_getter.h"
  17. #include "remoting/base/string_resources.h"
  18. #include "remoting/client/connect_to_host_info.h"
  19. #import "remoting/ios/app/account_manager.h"
  20. #import "remoting/ios/app/app_delegate.h"
  21. #import "remoting/ios/app/client_connection_view_controller.h"
  22. #import "remoting/ios/app/host_collection_view_controller.h"
  23. #import "remoting/ios/app/host_fetching_error_view_controller.h"
  24. #import "remoting/ios/app/host_fetching_view_controller.h"
  25. #import "remoting/ios/app/host_setup_view_controller.h"
  26. #import "remoting/ios/app/host_view_controller.h"
  27. #import "remoting/ios/app/refresh_control_provider.h"
  28. #import "remoting/ios/app/remoting_theme.h"
  29. #import "remoting/ios/app/view_utils.h"
  30. #import "remoting/ios/domain/client_session_details.h"
  31. #include "remoting/ios/facade/host_list_service.h"
  32. #import "remoting/ios/facade/remoting_service.h"
  33. #include "ui/base/l10n/l10n_util.h"
  34. #if !defined(__has_feature) || !__has_feature(objc_arc)
  35. #error "This file requires ARC support."
  36. #endif
  37. static CGFloat kHostInset = 5.f;
  38. namespace {
  39. #pragma mark - Network Reachability
  40. enum class ConnectionType {
  41. UNKNOWN,
  42. NONE,
  43. WWAN,
  44. WIFI,
  45. };
  46. ConnectionType GetConnectionType() {
  47. // 0.0.0.0 is a special token that causes reachability to monitor the general
  48. // routing status of the device, both IPv4 and IPv6.
  49. struct sockaddr_in addr = {0};
  50. addr.sin_len = sizeof(addr);
  51. addr.sin_family = AF_INET;
  52. base::ScopedCFTypeRef<SCNetworkReachabilityRef> reachability(
  53. SCNetworkReachabilityCreateWithAddress(
  54. kCFAllocatorDefault, reinterpret_cast<struct sockaddr*>(&addr)));
  55. SCNetworkReachabilityFlags flags;
  56. BOOL success = SCNetworkReachabilityGetFlags(reachability, &flags);
  57. if (!success) {
  58. return ConnectionType::UNKNOWN;
  59. }
  60. BOOL isReachable = flags & kSCNetworkReachabilityFlagsReachable;
  61. BOOL needsConnection = flags & kSCNetworkReachabilityFlagsConnectionRequired;
  62. BOOL isNetworkReachable = isReachable && !needsConnection;
  63. if (!isNetworkReachable) {
  64. return ConnectionType::NONE;
  65. } else if (flags & kSCNetworkReachabilityFlagsIsWWAN) {
  66. return ConnectionType::WWAN;
  67. }
  68. return ConnectionType::WIFI;
  69. }
  70. } // namespace
  71. #pragma mark - RemotingViewController
  72. using remoting::HostListService;
  73. @interface RemotingViewController ()<HostCollectionViewControllerDelegate,
  74. UIViewControllerAnimatedTransitioning,
  75. UIViewControllerTransitioningDelegate> {
  76. MDCDialogTransitionController* _dialogTransitionController;
  77. MDCAppBarViewController* _appBarViewController;
  78. HostCollectionViewController* _collectionViewController;
  79. HostFetchingViewController* _fetchingViewController;
  80. HostFetchingErrorViewController* _fetchingErrorViewController;
  81. HostSetupViewController* _setupViewController;
  82. HostListService* _hostListService;
  83. base::CallbackListSubscription _hostListStateSubscription;
  84. base::CallbackListSubscription _hostListFetchFailureSubscription;
  85. NSArray<id<RemotingRefreshControl>>* _refreshControls;
  86. }
  87. @end
  88. @implementation RemotingViewController
  89. - (instancetype)init {
  90. UICollectionViewFlowLayout* layout =
  91. [[MDCCollectionViewFlowLayout alloc] init];
  92. layout.minimumInteritemSpacing = 0;
  93. CGFloat sectionInset = kHostInset * 2.f;
  94. [layout setSectionInset:UIEdgeInsetsMake(sectionInset, sectionInset,
  95. sectionInset, sectionInset)];
  96. self = [super init];
  97. if (self) {
  98. _hostListService = HostListService::GetInstance();
  99. __weak RemotingViewController* weakSelf = self;
  100. RemotingRefreshAction refreshAction = ^{
  101. [weakSelf didSelectRefresh];
  102. };
  103. _collectionViewController = [[HostCollectionViewController alloc]
  104. initWithCollectionViewLayout:layout];
  105. _collectionViewController.delegate = self;
  106. _collectionViewController.scrollViewDelegate = self.headerViewController;
  107. _fetchingViewController = [[HostFetchingViewController alloc] init];
  108. _fetchingErrorViewController =
  109. [[HostFetchingErrorViewController alloc] init];
  110. _fetchingErrorViewController.onRetryCallback = refreshAction;
  111. _setupViewController = [[HostSetupViewController alloc] init];
  112. _setupViewController.scrollViewDelegate = self.headerViewController;
  113. _appBarViewController = [[MDCAppBarViewController alloc] init];
  114. [self addChildViewController:_appBarViewController];
  115. self.navigationItem.title =
  116. l10n_util::GetNSString(IDS_PRODUCT_NAME).lowercaseString;
  117. [self.navigationItem setHidesBackButton:YES animated:NO];
  118. _appBarViewController.headerView.backgroundColor =
  119. RemotingTheme.hostListBackgroundColor;
  120. _appBarViewController.navigationBar.backgroundColor =
  121. RemotingTheme.hostListBackgroundColor;
  122. MDCNavigationBarTextColorAccessibilityMutator* mutator =
  123. [[MDCNavigationBarTextColorAccessibilityMutator alloc] init];
  124. [mutator mutate:_appBarViewController.navigationBar];
  125. MDCFlexibleHeaderView* headerView = self.headerViewController.headerView;
  126. headerView.backgroundColor = [UIColor clearColor];
  127. // Use a custom shadow under the flexible header.
  128. MDCShadowLayer* shadowLayer = [MDCShadowLayer layer];
  129. [headerView setShadowLayer:shadowLayer
  130. intensityDidChangeBlock:^(CALayer* layer, CGFloat intensity) {
  131. CGFloat elevation = MDCShadowElevationAppBar * intensity;
  132. [(MDCShadowLayer*)layer setElevation:elevation];
  133. }];
  134. _refreshControls = @[
  135. [[RefreshControlProvider instance]
  136. createForScrollView:_collectionViewController.collectionView
  137. actionBlock:refreshAction],
  138. [[RefreshControlProvider instance]
  139. createForScrollView:_setupViewController.tableView
  140. actionBlock:refreshAction],
  141. ];
  142. }
  143. return self;
  144. }
  145. - (void)dealloc {
  146. [NSNotificationCenter.defaultCenter removeObserver:self];
  147. }
  148. #pragma mark - UIViewController
  149. - (void)viewDidLoad {
  150. [super viewDidLoad];
  151. UIImage* image = [UIImage imageNamed:@"Background"];
  152. UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
  153. [self.view addSubview:imageView];
  154. [self.view sendSubviewToBack:imageView];
  155. imageView.translatesAutoresizingMaskIntoConstraints = NO;
  156. [self.view addSubview:_appBarViewController.view];
  157. [_appBarViewController didMoveToParentViewController:self];
  158. UIViewController* accountParticleDiscViewController =
  159. remoting::ios::AccountManager::GetInstance()
  160. ->CreateAccountParticleDiscViewController();
  161. accountParticleDiscViewController.view
  162. .translatesAutoresizingMaskIntoConstraints = NO;
  163. [self addChildViewController:accountParticleDiscViewController];
  164. [self.view addSubview:accountParticleDiscViewController.view];
  165. [accountParticleDiscViewController didMoveToParentViewController:self];
  166. [NSLayoutConstraint activateConstraints:@[
  167. [[imageView widthAnchor]
  168. constraintGreaterThanOrEqualToAnchor:[self.view widthAnchor]],
  169. [[imageView heightAnchor]
  170. constraintGreaterThanOrEqualToAnchor:[self.view heightAnchor]],
  171. [accountParticleDiscViewController.view.topAnchor
  172. constraintEqualToAnchor:_appBarViewController.navigationBar.topAnchor],
  173. [accountParticleDiscViewController.view.trailingAnchor
  174. constraintEqualToAnchor:_appBarViewController.navigationBar
  175. .trailingAnchor],
  176. [accountParticleDiscViewController.view.widthAnchor
  177. constraintEqualToConstant:accountParticleDiscViewController
  178. .preferredContentSize.width],
  179. [accountParticleDiscViewController.view.heightAnchor
  180. constraintEqualToConstant:accountParticleDiscViewController
  181. .preferredContentSize.height],
  182. ]];
  183. __weak __typeof(self) weakSelf = self;
  184. _hostListStateSubscription =
  185. _hostListService->RegisterHostListStateCallback(base::BindRepeating(^{
  186. [weakSelf hostListStateDidChange];
  187. }));
  188. _hostListFetchFailureSubscription =
  189. _hostListService->RegisterFetchFailureCallback(base::BindRepeating(^{
  190. [weakSelf hostListFetchDidFail];
  191. }));
  192. }
  193. - (void)viewWillAppear:(BOOL)animated {
  194. [super viewWillAppear:animated];
  195. // Just in case the view controller misses the host list state event before
  196. // the listener is registered.
  197. [self refreshContent];
  198. _hostListService->RequestFetch();
  199. [NSNotificationCenter.defaultCenter
  200. addObserver:self
  201. selector:@selector(applicationDidBecomeActive:)
  202. name:UIApplicationDidBecomeActiveNotification
  203. object:nil];
  204. }
  205. - (void)viewWillDisappear:(BOOL)animated {
  206. [super viewWillDisappear:animated];
  207. [NSNotificationCenter.defaultCenter
  208. removeObserver:self
  209. name:UIApplicationDidBecomeActiveNotification
  210. object:nil];
  211. }
  212. - (UIStatusBarStyle)preferredStatusBarStyle {
  213. return UIStatusBarStyleLightContent;
  214. }
  215. #pragma mark - HostListService Callbacks
  216. - (void)hostListStateDidChange {
  217. [self refreshContent];
  218. }
  219. - (void)hostListFetchDidFail {
  220. [self handleHostListFetchFailure];
  221. }
  222. #pragma mark - HostCollectionViewControllerDelegate
  223. - (void)didSelectCell:(HostCollectionViewCell*)cell
  224. completion:(void (^)())completionBlock {
  225. if (![cell.hostInfo isOnline]) {
  226. MDCSnackbarMessage* message = [[MDCSnackbarMessage alloc] init];
  227. message.text = l10n_util::GetNSString(IDS_HOST_OFFLINE_TOOLTIP);
  228. [MDCSnackbarManager.defaultManager showMessage:message];
  229. return;
  230. }
  231. if (GetConnectionType() == ConnectionType::NONE) {
  232. [MDCSnackbarManager.defaultManager
  233. showMessage:[MDCSnackbarMessage
  234. messageWithText:l10n_util::GetNSString(
  235. IDS_ERROR_NETWORK_ERROR)]];
  236. return;
  237. }
  238. [MDCSnackbarManager.defaultManager
  239. dismissAndCallCompletionBlocksWithCategory:nil];
  240. ClientConnectionViewController* clientConnectionViewController =
  241. [[ClientConnectionViewController alloc] initWithHostInfo:cell.hostInfo];
  242. [self.navigationController pushViewController:clientConnectionViewController
  243. animated:YES];
  244. completionBlock();
  245. }
  246. - (NSInteger)getHostCount {
  247. return _hostListService->hosts().size();
  248. }
  249. - (HostInfo*)getHostAtIndexPath:(NSIndexPath*)path {
  250. return [[HostInfo alloc]
  251. initWithRemotingHostInfo:_hostListService->hosts()[path.row]];
  252. }
  253. #pragma mark - UIViewControllerTransitioningDelegate
  254. - (nullable id<UIViewControllerAnimatedTransitioning>)
  255. animationControllerForDismissedController:(UIViewController*)dismissed {
  256. return self;
  257. }
  258. #pragma mark - UIViewControllerAnimatedTransitioning
  259. - (void)animateTransition:
  260. (id<UIViewControllerContextTransitioning>)transitionContext {
  261. }
  262. - (NSTimeInterval)transitionDuration:
  263. (id<UIViewControllerContextTransitioning>)transitionContext {
  264. return 0.2;
  265. }
  266. #pragma mark - Private
  267. - (void)didSelectRefresh {
  268. _hostListService->RequestFetch();
  269. }
  270. - (void)refreshContent {
  271. if (_hostListService->state() == HostListService::State::FETCHING) {
  272. // We don't need to show the fetching view when either the host list or the
  273. // setup view is already shown. Refresh control will handle the
  274. // user-triggered refresh, and we don't need to show anything if
  275. // that's a background refresh (e.g. user just closed the session).
  276. if (self.contentViewController != _collectionViewController &&
  277. self.contentViewController != _setupViewController) {
  278. self.contentViewController = _fetchingViewController;
  279. }
  280. return;
  281. }
  282. if (_hostListService->state() == HostListService::State::NOT_FETCHED) {
  283. if (!_hostListService->last_fetch_failure()) {
  284. self.contentViewController = nil;
  285. } else {
  286. // hostListFetchDidFailNotification might miss the first failure happened
  287. // before the notification is registered. This logic covers that.
  288. [self handleHostListFetchFailure];
  289. }
  290. return;
  291. }
  292. DCHECK(_hostListService->state() == HostListService::State::FETCHED);
  293. [self stopAllRefreshControls];
  294. if (_hostListService->hosts().size() > 0) {
  295. [_collectionViewController.collectionView reloadData];
  296. self.headerViewController.headerView.trackingScrollView =
  297. _collectionViewController.collectionView;
  298. self.contentViewController = _collectionViewController;
  299. } else {
  300. self.headerViewController.headerView.trackingScrollView =
  301. _setupViewController.tableView;
  302. self.contentViewController = _setupViewController;
  303. }
  304. self.contentViewController.view.frame = self.view.bounds;
  305. }
  306. - (void)handleHostListFetchFailure {
  307. const auto* failure = _hostListService->last_fetch_failure();
  308. if (!failure) {
  309. return;
  310. }
  311. NSString* errorText = base::SysUTF8ToNSString(failure->localized_description);
  312. if ([self isAnyRefreshControlRefreshing]) {
  313. // User could just try pull-to-refresh again to refresh. We just need to
  314. // show the error as a toast.
  315. [MDCSnackbarManager.defaultManager
  316. showMessage:[MDCSnackbarMessage messageWithText:errorText]];
  317. [self stopAllRefreshControls];
  318. return;
  319. }
  320. // Pull-to-refresh is not available. We need to show a dedicated view to allow
  321. // user to retry.
  322. // Dismiss snackbars and so that the accessibility focus can shift into the
  323. // label.
  324. // TODO(yuweih): See if we really need to hide the account menu in this case,
  325. // since it requires nontrivial changes.
  326. [MDCSnackbarManager.defaultManager
  327. dismissAndCallCompletionBlocksWithCategory:nil];
  328. _fetchingErrorViewController.label.text = errorText;
  329. remoting::SetAccessibilityFocusElement(_fetchingErrorViewController.label);
  330. self.contentViewController = _fetchingErrorViewController;
  331. }
  332. - (BOOL)isAnyRefreshControlRefreshing {
  333. for (id<RemotingRefreshControl> control in _refreshControls) {
  334. if (control.isRefreshing) {
  335. return YES;
  336. }
  337. }
  338. return NO;
  339. }
  340. - (void)stopAllRefreshControls {
  341. for (id<RemotingRefreshControl> control in _refreshControls) {
  342. [control endRefreshing];
  343. }
  344. }
  345. - (void)applicationDidBecomeActive:(UIApplication*)application {
  346. _hostListService->RequestFetch();
  347. }
  348. @end