1// Copyright 2018 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
5#if !defined(__has_feature) || !__has_feature(objc_arc)
6#error "This file requires ARC support."
7#endif
8
9#import "remoting/ios/facade/host_list_service.h"
10
11#import <CoreFoundation/CoreFoundation.h>
12
13#include <algorithm>
14
15#import "remoting/ios/domain/user_info.h"
16#import "remoting/ios/facade/remoting_authentication.h"
17#import "remoting/ios/facade/remoting_service.h"
18
19#include "base/bind.h"
20#include "base/logging.h"
21#include "remoting/base/directory_service_client.h"
22#include "remoting/base/protobuf_http_status.h"
23#include "remoting/base/string_resources.h"
24#include "remoting/base/task_util.h"
25#include "remoting/client/chromoting_client_runtime.h"
26#include "services/network/public/cpp/shared_url_loader_factory.h"
27#include "ui/base/l10n/l10n_util.h"
28
29namespace remoting {
30
31namespace {
32
33HostListService::FetchFailureReason MapError(
34    ProtobufHttpStatus::Code status_code) {
35  switch (status_code) {
36    case ProtobufHttpStatus::Code::UNAVAILABLE:
37    case ProtobufHttpStatus::Code::DEADLINE_EXCEEDED:
38      return HostListService::FetchFailureReason::NETWORK_ERROR;
39    case ProtobufHttpStatus::Code::PERMISSION_DENIED:
40    case ProtobufHttpStatus::Code::UNAUTHENTICATED:
41      return HostListService::FetchFailureReason::AUTH_ERROR;
42    default:
43      return HostListService::FetchFailureReason::UNKNOWN_ERROR;
44  }
45}
46
47// Returns true if |h1| should sort before |h2|.
48bool CompareHost(const apis::v1::HostInfo& h1, const apis::v1::HostInfo& h2) {
49  // Online hosts always sort before offline hosts.
50  if (h1.status() != h2.status()) {
51    return h1.status() == apis::v1::HostInfo_Status_ONLINE;
52  }
53
54  // Sort by host name.
55  int name_compare = h1.host_name().compare(h2.host_name());
56  if (name_compare != 0) {
57    return name_compare < 0;
58  }
59
60  // Sort by last seen time if names are identical.
61  return h1.last_seen_time() < h2.last_seen_time();
62}
63
64}  // namespace
65
66HostListService* HostListService::GetInstance() {
67  static base::NoDestructor<HostListService> instance;
68  return instance.get();
69}
70
71HostListService::HostListService()
72    : HostListService(ChromotingClientRuntime::GetInstance()
73                          ->CreateDirectoryServiceClient()) {}
74
75HostListService::HostListService(
76    base::SequenceBound<DirectoryServiceClient> directory_client) {
77  directory_client_ = std::move(directory_client);
78  Init();
79}
80
81HostListService::~HostListService() {
82  [NSNotificationCenter.defaultCenter removeObserver:user_update_observer_];
83}
84
85void HostListService::Init() {
86  auto weak_this = weak_factory_.GetWeakPtr();
87  user_update_observer_ = [NSNotificationCenter.defaultCenter
88      addObserverForName:kUserDidUpdate
89                  object:nil
90                   queue:nil
91              usingBlock:^(NSNotification* notification) {
92                UserInfo* user = notification.userInfo[kUserInfo];
93                if (weak_this) {
94                  weak_this->OnUserUpdated(user != nil);
95                }
96              }];
97}
98
99std::unique_ptr<HostListService::CallbackSubscription>
100HostListService::RegisterHostListStateCallback(
101    const base::RepeatingClosure& callback) {
102  return host_list_state_callbacks_.Add(callback);
103}
104
105std::unique_ptr<HostListService::CallbackSubscription>
106HostListService::RegisterFetchFailureCallback(
107    const base::RepeatingClosure& callback) {
108  return fetch_failure_callbacks_.Add(callback);
109}
110
111void HostListService::RequestFetch() {
112  if (state_ == State::FETCHING) {
113    return;
114  }
115  SetState(State::FETCHING);
116  PostWithCallback(FROM_HERE, &directory_client_,
117                   &DirectoryServiceClient::GetHostList,
118                   base::BindOnce(&HostListService::HandleHostListResult,
119                                  weak_factory_.GetWeakPtr()));
120}
121
122void HostListService::SetState(State state) {
123  if (state == state_) {
124    return;
125  }
126  if (state == State::NOT_FETCHED) {
127    hosts_ = {};
128  } else if (state == State::FETCHING || state == State::FETCHED) {
129    last_fetch_failure_.reset();
130  }
131  state_ = state;
132  host_list_state_callbacks_.Notify();
133}
134
135void HostListService::HandleHostListResult(
136    const ProtobufHttpStatus& status,
137    std::unique_ptr<apis::v1::GetHostListResponse> response) {
138  if (!status.ok()) {
139    HandleFetchFailure(status);
140    return;
141  }
142  hosts_.clear();
143  for (const auto& host : response->hosts()) {
144    hosts_.push_back(host);
145  }
146  std::sort(hosts_.begin(), hosts_.end(), &CompareHost);
147  SetState(State::FETCHED);
148}
149
150void HostListService::HandleFetchFailure(const ProtobufHttpStatus& status) {
151  SetState(State::NOT_FETCHED);
152
153  if (status.error_code() == ProtobufHttpStatus::Code::CANCELLED) {
154    return;
155  }
156
157  last_fetch_failure_ = std::make_unique<FetchFailureInfo>();
158  last_fetch_failure_->reason = MapError(status.error_code());
159
160  switch (last_fetch_failure_->reason) {
161    case FetchFailureReason::NETWORK_ERROR:
162      last_fetch_failure_->localized_description =
163          l10n_util::GetStringUTF8(IDS_ERROR_NETWORK_ERROR);
164      break;
165    case FetchFailureReason::AUTH_ERROR:
166      last_fetch_failure_->localized_description =
167          l10n_util::GetStringUTF8(IDS_ERROR_OAUTH_TOKEN_INVALID);
168      break;
169    default:
170      last_fetch_failure_->localized_description = status.error_message();
171  }
172  LOG(WARNING) << "Failed to fetch host list: "
173               << last_fetch_failure_->localized_description
174               << " reason: " << static_cast<int>(last_fetch_failure_->reason);
175  fetch_failure_callbacks_.Notify();
176  if (last_fetch_failure_->reason == FetchFailureReason::AUTH_ERROR) {
177    [RemotingService.instance.authentication logout];
178  }
179}
180
181void HostListService::OnUserUpdated(bool is_user_signed_in) {
182  directory_client_.Post(FROM_HERE,
183                         &DirectoryServiceClient::CancelPendingRequests);
184  SetState(State::NOT_FETCHED);
185  if (is_user_signed_in) {
186    RequestFetch();
187  }
188}
189
190}  // namespace remoting
191