1// Copyright (c) 2012 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#include "chrome/browser/extensions/api/dial/dial_registry.h"
6
7#include "base/memory/scoped_ptr.h"
8#include "base/stl_util.h"
9#include "base/strings/string_number_conversions.h"
10#include "base/time/time.h"
11#include "base/values.h"
12#include "chrome/browser/browser_process.h"
13#include "chrome/browser/extensions/api/dial/dial_api.h"
14#include "chrome/browser/extensions/api/dial/dial_device_data.h"
15#include "chrome/browser/extensions/api/dial/dial_service.h"
16#include "chrome/browser/net/chrome_net_log.h"
17#include "chrome/common/extensions/api/dial.h"
18
19using base::Time;
20using base::TimeDelta;
21using net::NetworkChangeNotifier;
22
23namespace extensions {
24
25DialRegistry::DialRegistry(Observer* dial_api,
26                           const base::TimeDelta& refresh_interval,
27                           const base::TimeDelta& expiration,
28                           const size_t max_devices)
29  : num_listeners_(0),
30    discovery_generation_(0),
31    registry_generation_(0),
32    last_event_discovery_generation_(0),
33    last_event_registry_generation_(0),
34    label_count_(0),
35    refresh_interval_delta_(refresh_interval),
36    expiration_delta_(expiration),
37    max_devices_(max_devices),
38    dial_api_(dial_api) {
39  DCHECK(max_devices_ > 0);
40  NetworkChangeNotifier::AddNetworkChangeObserver(this);
41}
42
43DialRegistry::~DialRegistry() {
44  DCHECK(thread_checker_.CalledOnValidThread());
45  DCHECK_EQ(0, num_listeners_);
46  NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
47}
48
49DialService* DialRegistry::CreateDialService() {
50  DCHECK(g_browser_process->net_log());
51  return new DialServiceImpl(g_browser_process->net_log());
52}
53
54void DialRegistry::ClearDialService() {
55  dial_.reset();
56}
57
58base::Time DialRegistry::Now() const {
59  return Time::Now();
60}
61
62void DialRegistry::OnListenerAdded() {
63  DCHECK(thread_checker_.CalledOnValidThread());
64  if (++num_listeners_ == 1) {
65    VLOG(2) << "Listener added; starting periodic discovery.";
66    StartPeriodicDiscovery();
67  }
68}
69
70void DialRegistry::OnListenerRemoved() {
71  DCHECK(thread_checker_.CalledOnValidThread());
72  DCHECK(num_listeners_ > 0);
73  if (--num_listeners_ == 0) {
74    VLOG(2) << "Listeners removed; stopping periodic discovery.";
75    StopPeriodicDiscovery();
76  }
77}
78
79bool DialRegistry::ReadyToDiscover() {
80  if (num_listeners_ == 0) {
81    dial_api_->OnDialError(DIAL_NO_LISTENERS);
82    return false;
83  }
84  if (NetworkChangeNotifier::IsOffline()) {
85    dial_api_->OnDialError(DIAL_NETWORK_DISCONNECTED);
86    return false;
87  }
88  if (NetworkChangeNotifier::IsConnectionCellular(
89          NetworkChangeNotifier::GetConnectionType())) {
90    dial_api_->OnDialError(DIAL_CELLULAR_NETWORK);
91    return false;
92  }
93  return true;
94}
95
96bool DialRegistry::DiscoverNow() {
97  DCHECK(thread_checker_.CalledOnValidThread());
98  if (!ReadyToDiscover()) {
99    return false;
100  }
101  if (!dial_.get()) {
102    dial_api_->OnDialError(DIAL_UNKNOWN);
103    return false;
104  }
105
106  if (!dial_->HasObserver(this))
107    NOTREACHED() << "DiscoverNow() called without observing dial_";
108  discovery_generation_++;
109  return dial_->Discover();
110}
111
112void DialRegistry::StartPeriodicDiscovery() {
113  DCHECK(thread_checker_.CalledOnValidThread());
114  if (!ReadyToDiscover() || dial_.get())
115    return;
116
117  dial_.reset(CreateDialService());
118  dial_->AddObserver(this);
119  DoDiscovery();
120  repeating_timer_.Start(FROM_HERE,
121                         refresh_interval_delta_,
122                         this,
123                         &DialRegistry::DoDiscovery);
124}
125
126void DialRegistry::DoDiscovery() {
127  DCHECK(thread_checker_.CalledOnValidThread());
128  DCHECK(dial_.get());
129  discovery_generation_++;
130  VLOG(2) << "About to discover! Generation = " << discovery_generation_;
131  dial_->Discover();
132}
133
134void DialRegistry::StopPeriodicDiscovery() {
135  DCHECK(thread_checker_.CalledOnValidThread());
136  if (!dial_.get())
137    return;
138
139  repeating_timer_.Stop();
140  dial_->RemoveObserver(this);
141  ClearDialService();
142}
143
144bool DialRegistry::PruneExpiredDevices() {
145  DCHECK(thread_checker_.CalledOnValidThread());
146  bool pruned_device = false;
147  DeviceByLabelMap::iterator i = device_by_label_map_.begin();
148  while (i != device_by_label_map_.end()) {
149    linked_ptr<DialDeviceData> device = i->second;
150    if (IsDeviceExpired(*device)) {
151      VLOG(2) << "Device " << device->label() << " expired, removing";
152      const size_t num_erased_by_id =
153          device_by_id_map_.erase(device->device_id());
154      DCHECK_EQ(num_erased_by_id, 1u);
155      device_by_label_map_.erase(i++);
156      pruned_device = true;
157    } else {
158      ++i;
159    }
160  }
161  return pruned_device;
162}
163
164bool DialRegistry::IsDeviceExpired(const DialDeviceData& device) const {
165  Time now = Now();
166
167  // Check against our default expiration timeout.
168  Time default_expiration_time = device.response_time() + expiration_delta_;
169  if (now > default_expiration_time)
170    return true;
171
172  // Check against the device's cache-control header, if set.
173  if (device.has_max_age()) {
174    Time max_age_expiration_time =
175      device.response_time() + TimeDelta::FromSeconds(device.max_age());
176    if (now > max_age_expiration_time)
177      return true;
178  }
179  return false;
180}
181
182void DialRegistry::Clear() {
183  DCHECK(thread_checker_.CalledOnValidThread());
184  device_by_id_map_.clear();
185  device_by_label_map_.clear();
186  registry_generation_++;
187}
188
189void DialRegistry::MaybeSendEvent() {
190  DCHECK(thread_checker_.CalledOnValidThread());
191
192  // We need to send an event if:
193  // (1) We haven't sent one yet in this round of discovery, or
194  // (2) The device list changed since the last MaybeSendEvent.
195  bool needs_event =
196      (last_event_discovery_generation_ < discovery_generation_ ||
197       last_event_registry_generation_ < registry_generation_);
198  VLOG(2) << "ledg = " << last_event_discovery_generation_ << ", dg = "
199          << discovery_generation_
200          << ", lerg = " << last_event_registry_generation_ << ", rg = "
201          << registry_generation_
202          << ", needs_event = " << needs_event;
203  if (!needs_event)
204    return;
205
206  DeviceList device_list;
207  for (DeviceByLabelMap::const_iterator i = device_by_label_map_.begin();
208       i != device_by_label_map_.end(); i++) {
209    device_list.push_back(*(i->second));
210  }
211  dial_api_->OnDialDeviceEvent(device_list);
212
213  // Reset watermarks.
214  last_event_discovery_generation_ = discovery_generation_;
215  last_event_registry_generation_ = registry_generation_;
216}
217
218std::string DialRegistry::NextLabel() {
219  DCHECK(thread_checker_.CalledOnValidThread());
220  return base::IntToString(++label_count_);
221}
222
223void DialRegistry::OnDiscoveryRequest(DialService* service) {
224  DCHECK(thread_checker_.CalledOnValidThread());
225  MaybeSendEvent();
226}
227
228void DialRegistry::OnDeviceDiscovered(DialService* service,
229                                      const DialDeviceData& device) {
230  DCHECK(thread_checker_.CalledOnValidThread());
231
232  // Adds |device| to our list of devices or updates an existing device, unless
233  // |device| is a duplicate. Returns true if the list was modified and
234  // increments the list generation.
235  linked_ptr<DialDeviceData> device_data(new DialDeviceData(device));
236  DCHECK(!device_data->device_id().empty());
237  DCHECK(device_data->label().empty());
238
239  bool did_modify_list = false;
240  DeviceByIdMap::iterator lookup_result =
241      device_by_id_map_.find(device_data->device_id());
242
243  if (lookup_result != device_by_id_map_.end()) {
244    VLOG(2) << "Found device " << device_data->device_id() << ", merging";
245
246    // Already have previous response.  Merge in data from this response and
247    // track if there were any API visible changes.
248    did_modify_list = lookup_result->second->UpdateFrom(*device_data);
249  } else {
250    did_modify_list = MaybeAddDevice(device_data);
251  }
252
253  if (did_modify_list)
254    registry_generation_++;
255
256  VLOG(2) << "did_modify_list = " << did_modify_list
257          << ", generation = " << registry_generation_;
258}
259
260bool DialRegistry::MaybeAddDevice(
261    const linked_ptr<DialDeviceData>& device_data) {
262  DCHECK(thread_checker_.CalledOnValidThread());
263  if (device_by_id_map_.size() == max_devices_) {
264    VLOG(1) << "Maximum registry size reached.  Cannot add device.";
265    return false;
266  }
267  device_data->set_label(NextLabel());
268  device_by_id_map_[device_data->device_id()] = device_data;
269  device_by_label_map_[device_data->label()] = device_data;
270  VLOG(2) << "Added device, id = " << device_data->device_id()
271          << ", label = " << device_data->label();
272  return true;
273}
274
275void DialRegistry::OnDiscoveryFinished(DialService* service) {
276  DCHECK(thread_checker_.CalledOnValidThread());
277  if (PruneExpiredDevices())
278    registry_generation_++;
279  MaybeSendEvent();
280}
281
282void DialRegistry::OnError(DialService* service,
283                           const DialService::DialServiceErrorCode& code) {
284  DCHECK(thread_checker_.CalledOnValidThread());
285  switch (code) {
286    case DialService::DIAL_SERVICE_SOCKET_ERROR:
287      dial_api_->OnDialError(DIAL_SOCKET_ERROR);
288      break;
289    case DialService::DIAL_SERVICE_NO_INTERFACES:
290      dial_api_->OnDialError(DIAL_NO_INTERFACES);
291      break;
292    default:
293      NOTREACHED();
294      dial_api_->OnDialError(DIAL_UNKNOWN);
295      break;
296  }
297}
298
299void DialRegistry::OnNetworkChanged(
300    NetworkChangeNotifier::ConnectionType type) {
301  switch (type) {
302    case NetworkChangeNotifier::CONNECTION_NONE:
303      if (dial_.get()) {
304        VLOG(2) << "Lost connection, shutting down discovery and clearing"
305                << " list.";
306        dial_api_->OnDialError(DIAL_NETWORK_DISCONNECTED);
307
308        StopPeriodicDiscovery();
309        // TODO(justinlin): As an optimization, we can probably keep our device
310        // list around and restore it if we reconnected to the exact same
311        // network.
312        Clear();
313        MaybeSendEvent();
314      }
315      break;
316    case NetworkChangeNotifier::CONNECTION_2G:
317    case NetworkChangeNotifier::CONNECTION_3G:
318    case NetworkChangeNotifier::CONNECTION_4G:
319    case NetworkChangeNotifier::CONNECTION_ETHERNET:
320    case NetworkChangeNotifier::CONNECTION_WIFI:
321    case NetworkChangeNotifier::CONNECTION_UNKNOWN:
322    case NetworkChangeNotifier::CONNECTION_BLUETOOTH:
323      if (!dial_.get()) {
324        VLOG(2) << "Connection detected, restarting discovery.";
325        StartPeriodicDiscovery();
326      }
327      break;
328  }
329}
330
331}  // namespace extensions
332