buffet: Move ShillClient to buffet

ShillClient is brillo specific code and should not be in libweave.

BUG=brillo:1249
TEST='FEATURES=test emerge-gizmo buffet'

Change-Id: Ib44f3621519eb9de4cafae3042117b71de413846
Reviewed-on: https://chromium-review.googlesource.com/290100
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Vitaly Buka <vitalybuka@chromium.org>
Tested-by: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index 3c59e0a..fde6d30 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -33,6 +33,7 @@
         'dbus_constants.cc',
         'manager.cc',
         'peerd_client.cc',
+        'shill_client.cc',
         '../libweave/src/base_api_handler.cc',
         '../libweave/src/buffet_config.cc',
         '../libweave/src/commands/cloud_command_proxy.cc',
@@ -66,7 +67,6 @@
         '../libweave/src/privet/privet_types.cc',
         '../libweave/src/privet/publisher.cc',
         '../libweave/src/privet/security_manager.cc',
-        '../libweave/src/privet/shill_client.cc',
         '../libweave/src/privet/privet_types.cc',
         '../libweave/src/privet/wifi_bootstrap_manager.cc',
         '../libweave/src/privet/wifi_ssid_generator.cc',
diff --git a/buffet/main.cc b/buffet/main.cc
index 0d1dff9..276e030 100644
--- a/buffet/main.cc
+++ b/buffet/main.cc
@@ -24,19 +24,24 @@
 
 class Daemon final : public DBusServiceDaemon {
  public:
-  explicit Daemon(const weave::Device::Options& options)
-      : DBusServiceDaemon(kServiceName, kRootServicePath), options_{options} {}
+  Daemon(const weave::Device::Options& options,
+         const std::set<std::string>& device_whitelist)
+      : DBusServiceDaemon(kServiceName, kRootServicePath),
+        options_{options},
+        device_whitelist_{device_whitelist} {}
 
  protected:
   void RegisterDBusObjectsAsync(AsyncEventSequencer* sequencer) override {
     manager_.reset(new buffet::Manager(object_manager_->AsWeakPtr()));
-    manager_->Start(options_, sequencer);
+    manager_->Start(options_, device_whitelist_, sequencer);
   }
 
   void OnShutdown(int* return_code) override { manager_->Stop(); }
 
  private:
   weave::Device::Options options_;
+  std::set<std::string> device_whitelist_;
+
   std::unique_ptr<buffet::Manager> manager_;
   DISALLOW_COPY_AND_ASSIGN(Daemon);
 };
@@ -91,13 +96,12 @@
   options.definitions_path = base::FilePath{"/etc/buffet"};
   options.test_definitions_path = base::FilePath{FLAGS_test_definitions_path};
   options.xmpp_enabled = FLAGS_enable_xmpp;
-  options.device_whitelist.insert(device_whitelist.begin(),
-                                  device_whitelist.end());
   options.disable_privet = FLAGS_disable_privet;
   options.disable_security = FLAGS_disable_security;
   options.enable_ping = FLAGS_enable_ping;
   options.test_privet_ssid = FLAGS_test_privet_ssid;
 
-  buffet::Daemon daemon{options};
+  buffet::Daemon daemon{options, std::set<std::string>{device_whitelist.begin(),
+                                                       device_whitelist.end()}};
   return daemon.Run();
 }
diff --git a/buffet/manager.cc b/buffet/manager.cc
index f6b27a3..9df8caa 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -26,6 +26,7 @@
 #include "buffet/dbus_command_dispatcher.h"
 #include "buffet/dbus_conversion.h"
 #include "buffet/peerd_client.h"
+#include "buffet/shill_client.h"
 #include "weave/enum_to_string.h"
 
 using chromeos::dbus_utils::AsyncEventSequencer;
@@ -54,11 +55,14 @@
 }
 
 void Manager::Start(const weave::Device::Options& options,
+                    const std::set<std::string>& device_whitelist,
                     AsyncEventSequencer* sequencer) {
   peerd_client_.reset(new PeerdClient{dbus_object_.GetBus()});
+  shill_client_.reset(new ShillClient{dbus_object_.GetBus(), device_whitelist});
 
   device_ = weave::Device::Create();
-  device_->Start(options, peerd_client_.get(), &dbus_object_, sequencer);
+  device_->Start(options, peerd_client_.get(), shill_client_.get(),
+                 &dbus_object_, sequencer);
 
   command_dispatcher_.reset(new DBusCommandDispacher{
       dbus_object_.GetObjectManager(), device_->GetCommands()});
diff --git a/buffet/manager.h b/buffet/manager.h
index e9fcad9..c0037e9 100644
--- a/buffet/manager.h
+++ b/buffet/manager.h
@@ -30,9 +30,9 @@
 
 namespace buffet {
 
-class PeerdClient;
-
 class DBusCommandDispacher;
+class PeerdClient;
+class ShillClient;
 
 template<typename... Types>
 using DBusMethodResponsePtr =
@@ -53,6 +53,7 @@
   ~Manager();
 
   void Start(const weave::Device::Options& options,
+             const std::set<std::string>& device_whitelist,
              chromeos::dbus_utils::AsyncEventSequencer* sequencer);
 
   void Stop();
@@ -117,6 +118,7 @@
   chromeos::dbus_utils::DBusObject dbus_object_;
 
   std::unique_ptr<PeerdClient> peerd_client_;
+  std::unique_ptr<ShillClient> shill_client_;
   std::unique_ptr<weave::Device> device_;
   std::unique_ptr<DBusCommandDispacher> command_dispatcher_;
 
diff --git a/buffet/peerd_client.h b/buffet/peerd_client.h
index e24cd32..89322cd 100644
--- a/buffet/peerd_client.h
+++ b/buffet/peerd_client.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef BUFFET_PRIVET_PEERD_CLIENT_H_
-#define BUFFET_PRIVET_PEERD_CLIENT_H_
+#ifndef BUFFET_PEERD_CLIENT_H_
+#define BUFFET_PEERD_CLIENT_H_
 
 #include <map>
 #include <memory>
@@ -68,4 +68,4 @@
 
 }  // namespace buffet
 
-#endif  // BUFFET_PRIVET_PEERD_CLIENT_H_
+#endif  // BUFFET_PEERD_CLIENT_H_
diff --git a/buffet/shill_client.cc b/buffet/shill_client.cc
new file mode 100644
index 0000000..1efc52d
--- /dev/null
+++ b/buffet/shill_client.cc
@@ -0,0 +1,487 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/shill_client.h"
+
+#include <set>
+
+#include <base/message_loop/message_loop.h>
+#include <base/stl_util.h>
+#include <chromeos/any.h>
+#include <chromeos/dbus/service_constants.h>
+#include <chromeos/errors/error.h>
+#include <chromeos/errors/error_codes.h>
+
+#include "weave/enum_to_string.h"
+
+using chromeos::Any;
+using chromeos::VariantDictionary;
+using dbus::ObjectPath;
+using org::chromium::flimflam::DeviceProxy;
+using org::chromium::flimflam::ServiceProxy;
+using std::map;
+using std::set;
+using std::string;
+using std::vector;
+
+namespace buffet {
+
+namespace {
+
+void IgnoreDetachEvent() {}
+
+bool GetStateForService(ServiceProxy* service, string* state) {
+  CHECK(service) << "|service| was nullptr in GetStateForService()";
+  VariantDictionary properties;
+  if (!service->GetProperties(&properties, nullptr)) {
+    LOG(WARNING) << "Failed to read properties from service.";
+    return false;
+  }
+  auto property_it = properties.find(shill::kStateProperty);
+  if (property_it == properties.end()) {
+    LOG(WARNING) << "No state found in service properties.";
+    return false;
+  }
+  string new_state = property_it->second.TryGet<string>();
+  if (new_state.empty()) {
+    LOG(WARNING) << "Invalid state value.";
+    return false;
+  }
+  *state = new_state;
+  return true;
+}
+
+weave::NetworkState ShillServiceStateToNetworkState(const string& state) {
+  // TODO(wiley) What does "unconfigured" mean in a world with multiple sets
+  //             of WiFi credentials?
+  // TODO(wiley) Detect disabled devices, update state appropriately.
+  if ((state.compare(shill::kStateReady) == 0) ||
+      (state.compare(shill::kStatePortal) == 0) ||
+      (state.compare(shill::kStateOnline) == 0)) {
+    return weave::NetworkState::kConnected;
+  }
+  if ((state.compare(shill::kStateAssociation) == 0) ||
+      (state.compare(shill::kStateConfiguration) == 0)) {
+    return weave::NetworkState::kConnecting;
+  }
+  if ((state.compare(shill::kStateFailure) == 0) ||
+      (state.compare(shill::kStateActivationFailure) == 0)) {
+    // TODO(wiley) Get error information off the service object.
+    return weave::NetworkState::kFailure;
+  }
+  if ((state.compare(shill::kStateIdle) == 0) ||
+      (state.compare(shill::kStateOffline) == 0) ||
+      (state.compare(shill::kStateDisconnect) == 0)) {
+    return weave::NetworkState::kOffline;
+  }
+  LOG(WARNING) << "Unknown state found: '" << state << "'";
+  return weave::NetworkState::kOffline;
+}
+
+}  // namespace
+
+ShillClient::ShillClient(const scoped_refptr<dbus::Bus>& bus,
+                         const set<string>& device_whitelist)
+    : bus_{bus},
+      manager_proxy_{bus_, ObjectPath{"/"}},
+      device_whitelist_{device_whitelist} {
+  manager_proxy_.RegisterPropertyChangedSignalHandler(
+      base::Bind(&ShillClient::OnManagerPropertyChange,
+                 weak_factory_.GetWeakPtr()),
+      base::Bind(&ShillClient::OnManagerPropertyChangeRegistration,
+                 weak_factory_.GetWeakPtr()));
+  auto owner_changed_cb = base::Bind(&ShillClient::OnShillServiceOwnerChange,
+                                     weak_factory_.GetWeakPtr());
+  bus_->GetObjectProxy(shill::kFlimflamServiceName, ObjectPath{"/"})
+      ->SetNameOwnerChangedCallback(owner_changed_cb);
+}
+
+void ShillClient::Init() {
+  VLOG(2) << "ShillClient::Init();";
+  CleanupConnectingService(false);
+  devices_.clear();
+  connectivity_state_ = weave::NetworkState::kOffline;
+  VariantDictionary properties;
+  if (!manager_proxy_.GetProperties(&properties, nullptr)) {
+    LOG(ERROR) << "Unable to get properties from Manager, waiting for "
+                  "Manager to come back online.";
+    return;
+  }
+  auto it = properties.find(shill::kDevicesProperty);
+  CHECK(it != properties.end()) << "shill should always publish a device list.";
+  OnManagerPropertyChange(shill::kDevicesProperty, it->second);
+}
+
+bool ShillClient::ConnectToService(const string& ssid,
+                                   const string& passphrase,
+                                   const base::Closure& on_success,
+                                   chromeos::ErrorPtr* error) {
+  CleanupConnectingService(false);
+  VariantDictionary service_properties;
+  service_properties[shill::kTypeProperty] = Any{string{shill::kTypeWifi}};
+  service_properties[shill::kSSIDProperty] = Any{ssid};
+  service_properties[shill::kPassphraseProperty] = Any{passphrase};
+  service_properties[shill::kSecurityProperty] = Any{
+      string{passphrase.empty() ? shill::kSecurityNone : shill::kSecurityPsk}};
+  service_properties[shill::kSaveCredentialsProperty] = Any{true};
+  service_properties[shill::kAutoConnectProperty] = Any{true};
+  ObjectPath service_path;
+  if (!manager_proxy_.ConfigureService(service_properties, &service_path,
+                                       error)) {
+    return false;
+  }
+  if (!manager_proxy_.RequestScan(shill::kTypeWifi, error)) {
+    return false;
+  }
+  connecting_service_.reset(new ServiceProxy{bus_, service_path});
+  on_connect_success_.Reset(on_success);
+  connecting_service_->RegisterPropertyChangedSignalHandler(
+      base::Bind(&ShillClient::OnServicePropertyChange,
+                 weak_factory_.GetWeakPtr(), service_path),
+      base::Bind(&ShillClient::OnServicePropertyChangeRegistration,
+                 weak_factory_.GetWeakPtr(), service_path));
+  return true;
+}
+
+weave::NetworkState ShillClient::GetConnectionState() const {
+  return connectivity_state_;
+}
+
+void ShillClient::AddOnConnectionChangedCallback(
+    const OnConnectionChangedCallback& listener) {
+  connectivity_listeners_.push_back(listener);
+}
+
+bool ShillClient::IsMonitoredDevice(DeviceProxy* device) {
+  if (device_whitelist_.empty()) {
+    return true;
+  }
+  VariantDictionary device_properties;
+  if (!device->GetProperties(&device_properties, nullptr)) {
+    LOG(ERROR) << "Devices without properties aren't whitelisted.";
+    return false;
+  }
+  auto it = device_properties.find(shill::kInterfaceProperty);
+  if (it == device_properties.end()) {
+    LOG(ERROR) << "Failed to find interface property in device properties.";
+    return false;
+  }
+  return ContainsKey(device_whitelist_, it->second.TryGet<string>());
+}
+
+void ShillClient::OnShillServiceOwnerChange(const string& old_owner,
+                                            const string& new_owner) {
+  VLOG(1) << "Shill service owner name changed to '" << new_owner << "'";
+  if (new_owner.empty()) {
+    CleanupConnectingService(false);
+    devices_.clear();
+    connectivity_state_ = weave::NetworkState::kOffline;
+  } else {
+    Init();  // New service owner means shill reset!
+  }
+}
+
+void ShillClient::OnManagerPropertyChangeRegistration(const string& interface,
+                                                      const string& signal_name,
+                                                      bool success) {
+  VLOG(3) << "Registered ManagerPropertyChange handler.";
+  CHECK(success) << "privetd requires Manager signals.";
+  VariantDictionary properties;
+  if (!manager_proxy_.GetProperties(&properties, nullptr)) {
+    LOG(ERROR) << "Unable to get properties from Manager, waiting for "
+                  "Manager to come back online.";
+    return;
+  }
+  auto it = properties.find(shill::kDevicesProperty);
+  CHECK(it != properties.end()) << "Shill should always publish a device list.";
+  OnManagerPropertyChange(shill::kDevicesProperty, it->second);
+}
+
+void ShillClient::OnManagerPropertyChange(const string& property_name,
+                                          const Any& property_value) {
+  if (property_name != shill::kDevicesProperty) {
+    return;
+  }
+  VLOG(3) << "Manager's device list has changed.";
+  // We're going to remove every device we haven't seen in the update.
+  set<ObjectPath> device_paths_to_remove;
+  for (const auto& kv : devices_) {
+    device_paths_to_remove.insert(kv.first);
+  }
+  for (const auto& device_path : property_value.TryGet<vector<ObjectPath>>()) {
+    if (!device_path.IsValid()) {
+      LOG(ERROR) << "Ignoring invalid device path in Manager's device list.";
+      return;
+    }
+    auto it = devices_.find(device_path);
+    if (it != devices_.end()) {
+      // Found an existing proxy.  Since the whitelist never changes,
+      // this still a valid device.
+      device_paths_to_remove.erase(device_path);
+      continue;
+    }
+    std::unique_ptr<DeviceProxy> device{new DeviceProxy{bus_, device_path}};
+    if (!IsMonitoredDevice(device.get())) {
+      continue;
+    }
+    device->RegisterPropertyChangedSignalHandler(
+        base::Bind(&ShillClient::OnDevicePropertyChange,
+                   weak_factory_.GetWeakPtr(), device_path),
+        base::Bind(&ShillClient::OnDevicePropertyChangeRegistration,
+                   weak_factory_.GetWeakPtr(), device_path));
+    VLOG(3) << "Creating device proxy at " << device_path.value();
+    devices_[device_path].device = std::move(device);
+  }
+  // Clean up devices/services related to removed devices.
+  if (!device_paths_to_remove.empty()) {
+    for (const ObjectPath& device_path : device_paths_to_remove) {
+      devices_.erase(device_path);
+    }
+    UpdateConnectivityState();
+  }
+}
+
+void ShillClient::OnDevicePropertyChangeRegistration(
+    const ObjectPath& device_path,
+    const string& interface,
+    const string& signal_name,
+    bool success) {
+  VLOG(3) << "Registered DevicePropertyChange handler.";
+  auto it = devices_.find(device_path);
+  if (it == devices_.end()) {
+    return;
+  }
+  CHECK(success) << "Failed to subscribe to Device property changes.";
+  DeviceProxy* device = it->second.device.get();
+  VariantDictionary properties;
+  if (!device->GetProperties(&properties, nullptr)) {
+    LOG(WARNING) << "Failed to get device properties?";
+    return;
+  }
+  auto prop_it = properties.find(shill::kSelectedServiceProperty);
+  if (prop_it == properties.end()) {
+    LOG(WARNING) << "Failed to get device's selected service?";
+    return;
+  }
+  OnDevicePropertyChange(device_path, shill::kSelectedServiceProperty,
+                         prop_it->second);
+}
+
+void ShillClient::OnDevicePropertyChange(const ObjectPath& device_path,
+                                         const string& property_name,
+                                         const Any& property_value) {
+  // We only care about selected services anyway.
+  if (property_name != shill::kSelectedServiceProperty) {
+    return;
+  }
+  // If the device isn't our list of whitelisted devices, ignore it.
+  auto it = devices_.find(device_path);
+  if (it == devices_.end()) {
+    return;
+  }
+  DeviceState& device_state = it->second;
+  ObjectPath service_path{property_value.TryGet<ObjectPath>()};
+  if (!service_path.IsValid()) {
+    LOG(ERROR) << "Device at " << device_path.value()
+               << " selected invalid service path.";
+    return;
+  }
+  VLOG(3) << "Device at " << it->first.value() << " has selected service at "
+          << service_path.value();
+  bool removed_old_service{false};
+  if (device_state.selected_service) {
+    if (device_state.selected_service->GetObjectPath() == service_path) {
+      return;  // Spurious update?
+    }
+    device_state.selected_service.reset();
+    device_state.service_state = weave::NetworkState::kOffline;
+    removed_old_service = true;
+  }
+  std::shared_ptr<ServiceProxy> new_service;
+  const bool reuse_connecting_service =
+      service_path.value() != "/" && connecting_service_ &&
+      connecting_service_->GetObjectPath() == service_path;
+  if (reuse_connecting_service) {
+    new_service = connecting_service_;
+    // When we reuse the connecting service, we need to make sure that our
+    // cached state is correct.  Normally, we do this by relying reading the
+    // state when our signal handlers finish registering, but this may have
+    // happened long in the past for the connecting service.
+    string state;
+    if (GetStateForService(new_service.get(), &state)) {
+      device_state.service_state = ShillServiceStateToNetworkState(state);
+    } else {
+      LOG(WARNING) << "Failed to read properties from existing service "
+                      "on selection.";
+    }
+  } else if (service_path.value() != "/") {
+    // The device has selected a new service we haven't see before.
+    new_service.reset(new ServiceProxy{bus_, service_path});
+    new_service->RegisterPropertyChangedSignalHandler(
+        base::Bind(&ShillClient::OnServicePropertyChange,
+                   weak_factory_.GetWeakPtr(), service_path),
+        base::Bind(&ShillClient::OnServicePropertyChangeRegistration,
+                   weak_factory_.GetWeakPtr(), service_path));
+  }
+  device_state.selected_service = new_service;
+  if (reuse_connecting_service || removed_old_service) {
+    UpdateConnectivityState();
+  }
+}
+
+void ShillClient::OnServicePropertyChangeRegistration(const ObjectPath& path,
+                                                      const string& interface,
+                                                      const string& signal_name,
+                                                      bool success) {
+  VLOG(3) << "OnServicePropertyChangeRegistration(" << path.value() << ");";
+  ServiceProxy* service{nullptr};
+  if (connecting_service_ && connecting_service_->GetObjectPath() == path) {
+    // Note that the connecting service might also be a selected service.
+    service = connecting_service_.get();
+    if (!success) {
+      CleanupConnectingService(false);
+    }
+  } else {
+    for (const auto& kv : devices_) {
+      if (kv.second.selected_service &&
+          kv.second.selected_service->GetObjectPath() == path) {
+        service = kv.second.selected_service.get();
+        break;
+      }
+    }
+  }
+  if (service == nullptr || !success) {
+    return;  // A failure or success for a proxy we no longer care about.
+  }
+  VariantDictionary properties;
+  if (!service->GetProperties(&properties, nullptr)) {
+    return;
+  }
+  // Give ourselves property changed signals for the initial property
+  // values.
+  auto it = properties.find(shill::kStateProperty);
+  if (it != properties.end()) {
+    OnServicePropertyChange(path, shill::kStateProperty, it->second);
+  }
+  it = properties.find(shill::kSignalStrengthProperty);
+  if (it != properties.end()) {
+    OnServicePropertyChange(path, shill::kSignalStrengthProperty, it->second);
+  }
+}
+
+void ShillClient::OnServicePropertyChange(const ObjectPath& service_path,
+                                          const string& property_name,
+                                          const Any& property_value) {
+  VLOG(3) << "ServicePropertyChange(" << service_path.value() << ", "
+          << property_name << ", ...);";
+  if (property_name == shill::kStateProperty) {
+    const string state{property_value.TryGet<string>()};
+    if (state.empty()) {
+      VLOG(3) << "Invalid service state update.";
+      return;
+    }
+    VLOG(3) << "New service state=" << state;
+    OnStateChangeForSelectedService(service_path, state);
+    OnStateChangeForConnectingService(service_path, state);
+  } else if (property_name == shill::kSignalStrengthProperty) {
+    OnStrengthChangeForConnectingService(service_path,
+                                         property_value.TryGet<uint8_t>());
+  }
+}
+
+void ShillClient::OnStateChangeForConnectingService(
+    const ObjectPath& service_path,
+    const string& state) {
+  if (!connecting_service_ ||
+      connecting_service_->GetObjectPath() != service_path ||
+      ShillServiceStateToNetworkState(state) !=
+          weave::NetworkState::kConnected) {
+    return;
+  }
+  connecting_service_reset_pending_ = true;
+  on_connect_success_.callback().Run();
+  CleanupConnectingService(true);
+}
+
+void ShillClient::OnStrengthChangeForConnectingService(
+    const ObjectPath& service_path,
+    uint8_t signal_strength) {
+  if (!connecting_service_ ||
+      connecting_service_->GetObjectPath() != service_path ||
+      signal_strength <= 0 || have_called_connect_) {
+    return;
+  }
+  VLOG(1) << "Connecting service has signal. Calling Connect().";
+  have_called_connect_ = true;
+  // Failures here indicate that we've already connected,
+  // or are connecting, or some other very unexciting thing.
+  // Ignore all that, and rely on state changes to detect
+  // connectivity.
+  connecting_service_->Connect(nullptr);
+}
+
+void ShillClient::OnStateChangeForSelectedService(
+    const ObjectPath& service_path,
+    const string& state) {
+  // Find the device/service pair responsible for this update
+  VLOG(3) << "State for potentially selected service " << service_path.value()
+          << " have changed to " << state;
+  for (auto& kv : devices_) {
+    if (kv.second.selected_service &&
+        kv.second.selected_service->GetObjectPath() == service_path) {
+      VLOG(3) << "Updated cached connection state for selected service.";
+      kv.second.service_state = ShillServiceStateToNetworkState(state);
+      UpdateConnectivityState();
+      return;
+    }
+  }
+}
+
+void ShillClient::UpdateConnectivityState() {
+  // Update the connectivity state of the device by picking the
+  // state of the currently most connected selected service.
+  weave::NetworkState new_connectivity_state{weave::NetworkState::kOffline};
+  for (const auto& kv : devices_) {
+    if (kv.second.service_state > new_connectivity_state) {
+      new_connectivity_state = kv.second.service_state;
+    }
+  }
+  VLOG(1) << "Connectivity changed: " << EnumToString(connectivity_state_)
+          << " -> " << EnumToString(new_connectivity_state);
+  // Notify listeners even if state changed to the same value. Listeners may
+  // want to handle this event.
+  connectivity_state_ = new_connectivity_state;
+  // We may call UpdateConnectivityState whenever we mutate a data structure
+  // such that our connectivity status could change.  However, we don't want
+  // to allow people to call into ShillClient while some other operation is
+  // underway.  Therefore, call our callbacks later, when we're in a good
+  // state.
+  base::MessageLoop::current()->PostTask(
+      FROM_HERE,
+      base::Bind(&ShillClient::NotifyConnectivityListeners,
+                 weak_factory_.GetWeakPtr(),
+                 GetConnectionState() == weave::NetworkState::kConnected));
+}
+
+void ShillClient::NotifyConnectivityListeners(bool am_online) {
+  VLOG(3) << "Notifying connectivity listeners that online=" << am_online;
+  for (const auto& listener : connectivity_listeners_) {
+    listener.Run(am_online);
+  }
+}
+
+void ShillClient::CleanupConnectingService(bool check_for_reset_pending) {
+  if (check_for_reset_pending && !connecting_service_reset_pending_) {
+    return;  // Must have called connect before we got here.
+  }
+  if (connecting_service_) {
+    connecting_service_->ReleaseObjectProxy(base::Bind(&IgnoreDetachEvent));
+    connecting_service_.reset();
+  }
+  on_connect_success_.Cancel();
+  have_called_connect_ = false;
+  connecting_service_reset_pending_ = false;
+}
+
+}  // namespace buffet
diff --git a/buffet/shill_client.h b/buffet/shill_client.h
new file mode 100644
index 0000000..06c1be5
--- /dev/null
+++ b/buffet/shill_client.h
@@ -0,0 +1,115 @@
+// Copyright 2014 The Chromium OS 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 BUFFET_SHILL_CLIENT_H_
+#define BUFFET_SHILL_CLIENT_H_
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <base/callback.h>
+#include <base/cancelable_callback.h>
+#include <base/macros.h>
+#include <base/memory/ref_counted.h>
+#include <base/memory/weak_ptr.h>
+#include <dbus/bus.h>
+
+#include "shill/dbus-proxies.h"
+#include "weave/network.h"
+
+namespace buffet {
+
+class ShillClient final : public weave::Network {
+ public:
+  ShillClient(const scoped_refptr<dbus::Bus>& bus,
+              const std::set<std::string>& device_whitelist);
+  ~ShillClient() = default;
+
+  void Init();
+
+  // Network implementation.
+  void AddOnConnectionChangedCallback(
+      const OnConnectionChangedCallback& listener) override;
+  bool ConnectToService(const std::string& ssid,
+                        const std::string& passphrase,
+                        const base::Closure& on_success,
+                        chromeos::ErrorPtr* error) override;
+  weave::NetworkState GetConnectionState() const override;
+
+ private:
+  struct DeviceState {
+    std::unique_ptr<org::chromium::flimflam::DeviceProxy> device;
+    // ServiceProxy objects are shared because the connecting service will
+    // also be the selected service for a device, but is not always the selected
+    // service (for instance, in the period between configuring a WiFi service
+    // with credentials, and when Connect() is called.)
+    std::shared_ptr<org::chromium::flimflam::ServiceProxy> selected_service;
+    weave::NetworkState service_state{weave::NetworkState::kOffline};
+  };
+
+  bool IsMonitoredDevice(org::chromium::flimflam::DeviceProxy* device);
+  void OnShillServiceOwnerChange(const std::string& old_owner,
+                                 const std::string& new_owner);
+  void OnManagerPropertyChangeRegistration(const std::string& interface,
+                                           const std::string& signal_name,
+                                           bool success);
+  void OnManagerPropertyChange(const std::string& property_name,
+                               const chromeos::Any& property_value);
+  void OnDevicePropertyChangeRegistration(const dbus::ObjectPath& device_path,
+                                          const std::string& interface,
+                                          const std::string& signal_name,
+                                          bool success);
+  void OnDevicePropertyChange(const dbus::ObjectPath& device_path,
+                              const std::string& property_name,
+                              const chromeos::Any& property_value);
+  void OnServicePropertyChangeRegistration(const dbus::ObjectPath& path,
+                                           const std::string& interface,
+                                           const std::string& signal_name,
+                                           bool success);
+  void OnServicePropertyChange(const dbus::ObjectPath& service_path,
+                               const std::string& property_name,
+                               const chromeos::Any& property_value);
+
+  void OnStateChangeForConnectingService(const dbus::ObjectPath& service_path,
+                                         const std::string& state);
+  void OnStrengthChangeForConnectingService(
+      const dbus::ObjectPath& service_path,
+      uint8_t signal_strength);
+  void OnStateChangeForSelectedService(const dbus::ObjectPath& service_path,
+                                       const std::string& state);
+  void UpdateConnectivityState();
+  void NotifyConnectivityListeners(bool am_online);
+  // Clean up state related to a connecting service.  If
+  // |check_for_reset_pending| is set, then we'll check to see if we've called
+  // ConnectToService() in the time since a task to call this function was
+  // posted.
+  void CleanupConnectingService(bool check_for_reset_pending);
+
+  const scoped_refptr<dbus::Bus> bus_;
+  org::chromium::flimflam::ManagerProxy manager_proxy_;
+  // There is logic that assumes we will never change this device list
+  // in OnManagerPropertyChange.  Do not be tempted to remove this const.
+  const std::set<std::string> device_whitelist_;
+  std::vector<OnConnectionChangedCallback> connectivity_listeners_;
+
+  // State for tracking where we are in our attempts to connect to a service.
+  bool connecting_service_reset_pending_{false};
+  bool have_called_connect_{false};
+  std::shared_ptr<org::chromium::flimflam::ServiceProxy> connecting_service_;
+  base::CancelableClosure on_connect_success_;
+
+  // State for tracking our online connectivity.
+  std::map<dbus::ObjectPath, DeviceState> devices_;
+  weave::NetworkState connectivity_state_{weave::NetworkState::kOffline};
+
+  base::WeakPtrFactory<ShillClient> weak_factory_{this};
+
+  DISALLOW_COPY_AND_ASSIGN(ShillClient);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_SHILL_CLIENT_H_