Cashew: track cellular service state.

BUG=chromium-os:7190
TEST=libcros/monitor_network and manual testing, observing cashew logs

Change-Id: I60a175f72dc665e12ab0eaa1c87054ad4c2dfe8b

Review URL: http://codereview.chromium.org/3576011
diff --git a/Makefile.am b/Makefile.am
index 8d91b48..2a94be8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-SUBDIRS = introspection src
+SUBDIRS = extrospection introspection src
 
 EXTRA_DIST = \
   cashew.conf \
diff --git a/configure.ac b/configure.ac
index 579fc96..6c2c32c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -11,6 +11,7 @@
 AC_CONFIG_HEADERS([config.h])
 AC_CONFIG_FILES([
     Makefile
+    extrospection/Makefile
     introspection/Makefile
     src/Makefile
 ])
diff --git a/extrospection/Makefile.am b/extrospection/Makefile.am
new file mode 100644
index 0000000..7a976f2
--- /dev/null
+++ b/extrospection/Makefile.am
@@ -0,0 +1,8 @@
+# Copyright (c) 2010 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.
+
+EXTRA_DIST = \
+  flimflam-device.xml \
+  flimflam-manager.xml \
+  flimflam-service.xml
diff --git a/extrospection/flimflam-device.xml b/extrospection/flimflam-device.xml
new file mode 100644
index 0000000..0a2ab3b
--- /dev/null
+++ b/extrospection/flimflam-device.xml
@@ -0,0 +1,29 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+
+<!-- 
+     Introspection dump generated manually by poking flimflam with dbus-send
+     TODO(vlaviano): should be generated automatically during flimflam build
+     and installed somewhere where cashew and other clients can consume it.
+-->
+
+<node>
+	<interface name="org.chromium.flimflam.Device">
+		<method name="GetProperties">
+			<arg type="a{sv}" direction="out"/>
+		</method>
+		<method name="SetProperty">
+			<arg type="s" direction="in"/>
+			<arg type="v" direction="in"/>
+		</method>
+		<method name="ProposeScan"/>
+		<method name="AddIPConfig">
+			<arg type="s" direction="in"/>
+			<arg type="o" direction="out"/>
+		</method>
+		<signal name="PropertyChanged">
+			<arg type="s"/>
+			<arg type="v"/>
+		</signal>
+	</interface>
+</node>
diff --git a/extrospection/flimflam-manager.xml b/extrospection/flimflam-manager.xml
new file mode 100644
index 0000000..a7e21cd
--- /dev/null
+++ b/extrospection/flimflam-manager.xml
@@ -0,0 +1,86 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+
+<!-- 
+     Introspection dump generated manually by poking flimflam with dbus-send
+     TODO(vlaviano): should be generated automatically during flimflam build
+     and installed somewhere where cashew and other clients can consume it.
+-->
+
+<node>
+	<interface name="org.chromium.flimflam.Manager">
+		<method name="GetProperties">
+			<arg type="a{sv}" direction="out"/>
+		</method>
+		<method name="SetProperty">
+			<arg type="s" direction="in"/>
+			<arg type="v" direction="in"/>
+		</method>
+		<method name="GetState">
+			<arg type="s" direction="out"/>
+		</method>
+		<method name="CreateProfile">
+			<arg type="s" direction="in"/>
+			<arg type="o" direction="out"/>
+		</method>
+		<method name="RemoveProfile">
+			<arg type="o" direction="in"/>
+		</method>
+		<method name="RequestScan">
+			<arg type="s" direction="in"/>
+		</method>
+		<method name="EnableTechnology">
+			<arg type="s" direction="in"/>
+		</method>
+		<method name="DisableTechnology">
+			<arg type="s" direction="in"/>
+		</method>
+		<method name="GetService">
+			<arg type="a{sv}" direction="in"/>
+			<arg type="o" direction="out"/>
+		</method>
+		<method name="GetWifiService">
+			<arg type="a{sv}" direction="in"/>
+			<arg type="o" direction="out"/>
+		</method>
+		<method name="ConfigureWifiService">
+			<arg type="a{sv}" direction="in"/>
+		</method>
+		<method name="RegisterAgent">
+			<arg type="o" direction="in"/>
+		</method>
+		<method name="UnregisterAgent">
+			<arg type="o" direction="in"/>
+		</method>
+		<method name="GetDebugTags">
+			<arg type="s" direction="out"/>
+		</method>
+		<method name="SetDebugTags">
+			<arg type="s" direction="in"/>
+		</method>
+		<method name="ListDebugTags">
+			<arg type="s" direction="out"/>
+		</method>
+		<method name="GetDebugMask">
+			<arg type="u" direction="out"/>
+		</method>
+		<method name="SetDebugMask">
+			<arg type="u" direction="in"/>
+		</method>
+		<method name="GetServiceOrder">
+			<arg type="s" direction="out"/>
+		</method>
+		<method name="SetServiceOrder">
+			<arg type="s" direction="in"/>
+		</method>
+		<signal name="PropertyChanged">
+			<arg type="s"/>
+			<arg type="v"/>
+		</signal>
+		<signal name="StateChanged">
+			<arg type="s"/>
+		</signal>
+	</interface>
+	<node name="device"/>
+	<node name="profile"/>
+</node>
diff --git a/extrospection/flimflam-service.xml b/extrospection/flimflam-service.xml
new file mode 100644
index 0000000..6dd75ef
--- /dev/null
+++ b/extrospection/flimflam-service.xml
@@ -0,0 +1,39 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+
+<!-- 
+     Introspection dump generated manually by poking flimflam with dbus-send
+     TODO(vlaviano): should be generated automatically during flimflam build
+     and installed somewhere where cashew and other clients can consume it.
+-->
+
+<node>
+	<interface name="org.chromium.flimflam.Service">
+		<method name="GetProperties">
+			<arg type="a{sv}" direction="out"/>
+		</method>
+		<method name="SetProperty">
+			<arg type="s" direction="in"/>
+			<arg type="v" direction="in"/>
+		</method>
+		<method name="ClearProperty">
+			<arg type="s" direction="in"/>
+		</method>
+		<method name="Connect"/>
+		<method name="Disconnect"/>
+		<method name="Remove"/>
+		<method name="MoveBefore">
+			<arg type="o" direction="in"/>
+		</method>
+		<method name="MoveAfter">
+			<arg type="o" direction="in"/>
+		</method>
+		<method name="ActivateCellularModem">
+			<arg type="s" direction="in"/>
+		</method>
+		<signal name="PropertyChanged">
+			<arg type="s"/>
+			<arg type="v"/>
+		</signal>
+	</interface>
+</node>
diff --git a/introspection/cashew.xml b/introspection/cashew.xml
index 312fd0b..f546a28 100644
--- a/introspection/cashew.xml
+++ b/introspection/cashew.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 
-<node name="/" xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
+<node xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
   <interface name="org.chromium.Cashew">
 
     <tp:simple-type name="Timestamp" type="x">
@@ -87,6 +87,11 @@
           A list of plan structs describing the data plans for |service|.
         </tp:docstring>
       </arg>
+      <tp:possible-errors>
+        <tp:error name="org.chromium.Cashew.Error.ServiceUnknown">
+	  The service path does not correspond to a known service.
+	</tp:error>
+      </tp:possible-errors>
     </method>
 
     <method name="RequestDataPlansUpdate">
@@ -98,6 +103,11 @@
           The dbus path of the cellular service for which caller wants info.
         </tp:docstring>
       </arg>
+      <tp:possible-errors>
+        <tp:error name="org.chromium.Cashew.Error.ServiceUnknown">
+	  The service path does not correspond to a known service.
+	</tp:error>
+      </tp:possible-errors>
     </method>
 
     <method name="IsAlive">
diff --git a/src/Makefile.am b/src/Makefile.am
index 38d7580..26dd136 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -7,6 +7,14 @@
 cashewd_SOURCES = \
   cashew_server.h \
   cashew_server.cc \
+  data_plan.h \
+  data_plan.cc \
+  device.h \
+  device.cc \
+  service.h \
+  service.cc \
+  service_manager.h \
+  service_manager.cc \
   main.cc
 
 # D-Bus server stub generation
@@ -14,22 +22,38 @@
 cashew_server_glue.h: $(top_srcdir)/introspection/cashew.xml
 	$(DBUSXX_XML2CPP) $^ --adaptor=$@
 
+# D-Bus client stub generation
+flimflam_device_client_glue.h: $(top_srcdir)/extrospection/flimflam-device.xml
+	$(DBUSXX_XML2CPP) $^ --proxy=$@
+flimflam_manager_client_glue.h: $(top_srcdir)/extrospection/flimflam-manager.xml
+	$(DBUSXX_XML2CPP) $^ --proxy=$@
+flimflam_service_client_glue.h: $(top_srcdir)/extrospection/flimflam-service.xml
+	$(DBUSXX_XML2CPP) $^ --proxy=$@
+
 BUILT_SOURCES = \
-  cashew_server_glue.h
+  cashew_server_glue.h \
+  flimflam_device_client_glue.h \
+  flimflam_manager_client_glue.h \
+  flimflam_service_client_glue.h
 
 CLEANFILES = $(BUILT_SOURCES)
 
-# manually defining these instead of getting them via autoconf, because gflags
-# has no pkg-config file
+# manually defining info for libs with no pkg-config file
+
+LIBBASE_CFLAGS =
+LIBBASE_LIBS = -lbase
+
 GFLAGS_CFLAGS =
 GFLAGS_LIBS = -lgflags
 
 cashewd_CPPFLAGS = -Wall -Werror \
+  $(LIBBASE_CFLAGS) \
   @DBUS_CPP_CFLAGS@ \
   $(GFLAGS_CFLAGS) \
   @GLOG_CFLAGS@
 
 cashewd_LDADD = \
+  $(LIBBASE_LIBS) \
   @DBUS_CPP_LIBS@ \
   $(GFLAGS_LIBS) \
   @GLOG_LIBS@
diff --git a/src/cashew_server.cc b/src/cashew_server.cc
index a5c0aa4..54c5281 100644
--- a/src/cashew_server.cc
+++ b/src/cashew_server.cc
@@ -11,36 +11,52 @@
 const char* CashewServer::kServiceName = "org.chromium.Cashew";
 const char* CashewServer::kServicePath = "/org/chromium/Cashew";
 
-CashewServer::CashewServer(DBus::Connection& connection)  // NOLINT
-    : DBus::ObjectAdaptor(connection, kServicePath) {
+// Cashew D-Bus errors
+const char* kCashewErrorServiceUnknown =
+    "org.chromium.Cashew.Error.ServiceUnknown";
+
+CashewServer::CashewServer(DBus::Connection& connection,  // NOLINT
+                           const ServiceManager& service_manager)
+    : DBus::ObjectAdaptor(connection, kServicePath),
+      service_manager_(service_manager) {
+}
+
+CashewServer::~CashewServer() {
 }
 
 // Cashew D-Bus API methods
 
-DataPlanList CashewServer::GetDataPlans(const std::string& service,
-                                        DBus::Error& error) {  // NOLINT
-  DLOG(INFO) << "GetDataPlans: service = " << service;
-  // TODO(vlaviano): log and return error if we don't like service arg
-
-  // For now, we return hardcoded data.
-  // TODO(vlaviano): return real data.
-  DataPlan hardcoded_plan;
-  GetHardcodedDataPlan(&hardcoded_plan);
-  DataPlanList data_plans;
-  data_plans.push_back(hardcoded_plan);
-  return data_plans;
+DBusDataPlanList CashewServer::GetDataPlans(const std::string& service_path,
+                                            DBus::Error& error) {  // NOLINT
+  DLOG(INFO) << "GetDataPlans: service = " << service_path;
+  DBusDataPlanList dbus_data_plans;
+  const Service *service = service_manager_.GetService(service_path);
+  if (service == NULL) {
+    DLOG(WARNING) << "GetDataPlans: unknown service: " << service_path;
+    error.set(kCashewErrorServiceUnknown, service_path.c_str());
+    DCHECK(dbus_data_plans.empty());
+    return dbus_data_plans;
+  }
+  dbus_data_plans = service->GetDBusDataPlans();
+  return dbus_data_plans;
 }
 
-void CashewServer::RequestDataPlansUpdate(const std::string& service,
+void CashewServer::RequestDataPlansUpdate(const std::string& service_path,
                                           DBus::Error& error) {  // NOLINT
-  DLOG(INFO) <<  "RequestDataPlansUpdate: service = " << service;
-  // TODO(vlaviano): log and return error if we don't like service arg
+  DLOG(INFO) <<  "RequestDataPlansUpdate: service = " << service_path;
+  const Service *service = service_manager_.GetService(service_path);
+  if (service == NULL) {
+    DLOG(WARNING) << "RequestDataPlansUpdate: unknown service: "
+        << service_path;
+    error.set(kCashewErrorServiceUnknown, service_path.c_str());
+    return;
+  }
 
   // trigger a DataPlansUpdate signal
   // TODO(vlaviano): rate-limiting?
   // TODO(vlaviano): if cached data isn't recent, ask carrier API for fresh
   // data and wait for reply before sending signal.
-  EmitDataPlansUpdateSignal(service);
+  EmitDataPlansUpdateSignal(*service);
 }
 
 bool CashewServer::IsAlive(DBus::Error& error) {  // NOLINT
@@ -50,39 +66,12 @@
 
 // Private methods
 
-void CashewServer::EmitDataPlansUpdateSignal(const std::string& service) {
-  DLOG(INFO) << "EmitDataPlansUpdateSignal: service = " << service;
-  // TODO(vlaviano): log error and return if we don't like service arg
-
-  // For now, we return hardcoded data.
-  // TODO(vlaviano): return real data.
-  DataPlan hardcoded_plan;
-  GetHardcodedDataPlan(&hardcoded_plan);
-  DataPlanList data_plans;
-  data_plans.push_back(hardcoded_plan);
+void CashewServer::EmitDataPlansUpdateSignal(const Service& service) {
+  DLOG(INFO) << "EmitDataPlansUpdateSignal: service = " << service.GetPath();
+  DBusDataPlanList dbus_data_plans = service.GetDBusDataPlans();
 
   // send out the signal
-  this->DataPlansUpdate(service, data_plans);
-}
-
-void CashewServer::GetHardcodedDataPlan(DataPlan* plan) {
-  DCHECK(plan != NULL);
-
-  const std::string plan_name = "test";
-  const std::string plan_type = "UNLIMITED";
-  int64 plan_update_time = 0;
-  int64 plan_start_time = time(NULL) - 12*3600;  // 12 hours ago
-  int64 plan_end_time = time(NULL) + 12*3600;  // 12 hours hence
-  int64 plan_data_bytes = 100*1024*1024;  // 100 MB
-  int64 plan_data_bytes_used = 30*1024*1024;  // 30 MB
-
-  (*plan)["CellularPlanName"].writer().append_string(plan_name.c_str());
-  (*plan)["CellularPlanType"].writer().append_string(plan_type.c_str());
-  (*plan)["CellularPlanUpdateTime"].writer().append_int64(plan_update_time);
-  (*plan)["CellularPlanStart"].writer().append_int64(plan_start_time);
-  (*plan)["CellularPlanEnd"].writer().append_int64(plan_end_time);
-  (*plan)["CellularPlanDataBytes"].writer().append_int64(plan_data_bytes);
-  (*plan)["CellularDataBytesUsed"].writer().append_int64(plan_data_bytes_used);
+  DataPlansUpdate(service.GetPath(), dbus_data_plans);
 }
 
 }  // namespace cashew
diff --git a/src/cashew_server.h b/src/cashew_server.h
index 7bfc5bd..cf5a4e6 100644
--- a/src/cashew_server.h
+++ b/src/cashew_server.h
@@ -13,28 +13,29 @@
 #include <dbus-c++/dbus.h>  // NOLINT
 
 #include "src/cashew_server_glue.h"
+#include "src/data_plan.h"
+#include "src/service.h"
+#include "src/service_manager.h"
 
 namespace cashew {
 
-typedef std::map<std::string, DBus::Variant> DataPlan;
-typedef std::vector<DataPlan> DataPlanList;
-
 // Implements Cashew D-Bus API
 class CashewServer : public org::chromium::Cashew_adaptor,
                      public DBus::IntrospectableAdaptor,
                      public DBus::ObjectAdaptor {
   public:
-    explicit CashewServer(DBus::Connection& connection);  // NOLINT
-    virtual ~CashewServer() {}
+    CashewServer(DBus::Connection& connection,  // NOLINT
+                 const ServiceManager& service_manager);
+    virtual ~CashewServer();
 
     // Cashew D-Bus API methods
 
     // Get data plan info for a cellular service
-    virtual DataPlanList GetDataPlans(const std::string& service,
-                                      DBus::Error& error);  // NOLINT
+    virtual DBusDataPlanList GetDataPlans(const std::string& service_path,
+                                          DBus::Error& error);  // NOLINT
 
     // Request that a DataPlansUpdate signal be sent for a cellular service
-    virtual void RequestDataPlansUpdate(const std::string& service,
+    virtual void RequestDataPlansUpdate(const std::string& service_path,
                                         DBus::Error& error);  // NOLINT
 
     // Test method to verify that Cashew service is working
@@ -44,11 +45,10 @@
     static const char* kServicePath;
 
   private:
-    // Emit a DataPlansUpdate signal for a service
-    void EmitDataPlansUpdateSignal(const std::string& service);
+    const ServiceManager& service_manager_;
 
-    // Fill in data plan with hardcoded data for testing
-    void GetHardcodedDataPlan(DataPlan* data_plan);
+    // Emit a DataPlansUpdate signal for a service
+    void EmitDataPlansUpdateSignal(const Service& service);
 
     DISALLOW_COPY_AND_ASSIGN(CashewServer);
 };
diff --git a/src/data_plan.cc b/src/data_plan.cc
new file mode 100644
index 0000000..5b42d9d
--- /dev/null
+++ b/src/data_plan.cc
@@ -0,0 +1,102 @@
+// Copyright (c) 2010 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 "src/data_plan.h"
+
+#include <glog/logging.h>
+
+namespace cashew {
+
+// libcros CellularDataPlan property names
+static const char* kCellularDataPlanName = "CellularPlanName";
+static const char* kCellularDataPlanType = "CellularPlanType";
+static const char* kCellularDataPlanUpdateTime = "CellularPlanUpdateTime";
+static const char* kCellularDataPlanStartTime = "CellularPlanStart";
+static const char* kCellularDataPlanEndTime = "CellularPlanEnd";
+static const char* kCellularDataPlanDataBytesMax = "CellularPlanDataBytes";
+static const char* kCellularDataPlanDataBytesUsed = "CellularDataBytesUsed";
+
+// libcros CellularDataPlan type values
+static const char* kCellularDataPlanTypeUnlimited = "UNLIMITED";
+static const char* kCellularDataPlanTypeMeteredFree = "METERED_BASE";
+static const char* kCellularDataPlanTypeMeteredPaid = "METERED_PAID";
+
+DataPlan::DataPlan(const std::string& name, DataPlan::Type type,
+                   base::Time update_time, base::Time start_time,
+                   base::Time end_time, Bytes data_bytes_max,
+                   Bytes data_bytes_used)
+    : name_(name), type_(type), update_time_(update_time),
+      start_time_(start_time), end_time_(end_time),
+      data_bytes_max_(data_bytes_max), data_bytes_used_(data_bytes_used) {
+}
+
+DataPlan::~DataPlan() {
+}
+
+const std::string& DataPlan::GetName() const {
+  return name_;
+}
+
+DataPlan::Type DataPlan::GetType() const {
+  return type_;
+}
+
+base::Time DataPlan::GetUpdateTime() const {
+  return update_time_;
+}
+
+base::Time DataPlan::GetStartTime() const {
+  return start_time_;
+}
+
+base::Time DataPlan::GetEndTime() const {
+  return end_time_;
+}
+
+Bytes DataPlan::GetDataBytesMax() const {
+  return data_bytes_max_;
+}
+
+Bytes DataPlan::GetDataBytesUsed() const {
+  return data_bytes_used_;
+}
+
+DBusDataPlan DataPlan::ToDBusFormat() const {
+  DBusDataPlan plan;
+  // Indexing into map w/ nonexistent key causes empty DBus::Variant to be
+  // created and inserted. We then fill in the desired value via its writer
+  // DBus::MessageIterator.
+  plan[kCellularDataPlanName].writer().append_string(name_.c_str());
+  plan[kCellularDataPlanType].writer().append_string(TypeToString(type_));
+  plan[kCellularDataPlanUpdateTime].writer().append_int64(
+      update_time_.ToInternalValue());
+  plan[kCellularDataPlanStartTime].writer().append_int64(
+      start_time_.ToInternalValue());
+  plan[kCellularDataPlanEndTime].writer().append_int64(
+      end_time_.ToInternalValue());
+  // omit max bytes field for unlimited plans
+  // can libcros/Chrome deal? if not, can set this to int64 max.
+  if (type_ != kTypeUnlimited) {
+    plan[kCellularDataPlanDataBytesMax].writer().append_int64(data_bytes_max_);
+  }
+  plan[kCellularDataPlanDataBytesUsed].writer().append_int64(data_bytes_used_);
+  return plan;
+}
+
+// private methods
+
+const char* DataPlan::TypeToString(DataPlan::Type type) const {
+  switch (type) {
+    case kTypeUnlimited:
+      return kCellularDataPlanTypeUnlimited;
+    case kTypeMeteredFree:
+      return kCellularDataPlanTypeMeteredFree;
+    case kTypeMeteredPaid:
+      return kCellularDataPlanTypeMeteredPaid;
+    default:
+      DCHECK(false);
+  }
+}
+
+}  // namespace cashew
diff --git a/src/data_plan.h b/src/data_plan.h
new file mode 100644
index 0000000..47d2b2c
--- /dev/null
+++ b/src/data_plan.h
@@ -0,0 +1,103 @@
+// Copyright (c) 2010 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 SRC_DATA_PLAN_H_
+#define SRC_DATA_PLAN_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <base/basictypes.h>  // NOLINT
+#include <base/time.h>  // NOLINT
+#include <dbus-c++/dbus.h>  // NOLINT
+
+namespace cashew {
+
+class DataPlan;
+
+// byte count
+// TODO(vlaviano): this should be a uint64, but libcros/Chrome expects this
+typedef int64 Bytes;
+
+// List of DataPlan objects
+typedef std::vector<DataPlan*> DataPlanList;
+
+// DataPlan object formatted for D-Bus (using DBus::Variants to wrap fields)
+typedef std::map<std::string, DBus::Variant> DBusDataPlan;
+
+// List of DataPlan objects formatted for D-Bus
+typedef std::vector<DBusDataPlan> DBusDataPlanList;
+
+// Represents a data plan associated with a cellular service
+class DataPlan {
+  public:
+    // valid type values for this plan
+    typedef enum {
+      kTypeUnlimited = 0,
+      kTypeMeteredFree,
+      kTypeMeteredPaid,
+    } Type;
+
+    DataPlan(const std::string& name, DataPlan::Type type,
+             base::Time update_time, base::Time start_time,
+             base::Time end_time, Bytes data_bytes_max, Bytes data_bytes_used);
+    virtual ~DataPlan();
+
+    // get plan name
+    virtual const std::string& GetName() const;
+
+    // get plan type
+    virtual Type GetType() const;
+
+    // get last update time
+    virtual base::Time GetUpdateTime() const;
+
+    // get plan start time
+    virtual base::Time GetStartTime() const;
+
+    // get plan end time
+    virtual base::Time GetEndTime() const;
+
+    // get plan max bytes
+    virtual Bytes GetDataBytesMax() const;
+
+    // get plan used bytes
+    virtual Bytes GetDataBytesUsed() const;
+
+    // return a{sv} representation for D-Bus
+    virtual DBusDataPlan ToDBusFormat() const;
+
+  private:
+    // human-readable plan name
+    const std::string name_;
+
+    // plan type
+    const Type type_;
+
+    // last update time (i.e., how fresh is plan data)
+    base::Time update_time_;
+
+    // plan start time
+    const base::Time start_time_;
+
+    // plan end time
+    const base::Time end_time_;
+
+    // max bytes available during plan period
+    // irrelevant for plans of type kTypeUnlimited
+    const Bytes data_bytes_max_;
+
+    // bytes consumed so far during plan period
+    Bytes data_bytes_used_;
+
+    // convert plan type enum value to string
+    const char* TypeToString(Type) const;
+
+    DISALLOW_COPY_AND_ASSIGN(DataPlan);
+};
+
+}  // namespace cashew
+
+#endif  // SRC_DATA_PLAN_H_
diff --git a/src/device.cc b/src/device.cc
new file mode 100644
index 0000000..9f60c8e
--- /dev/null
+++ b/src/device.cc
@@ -0,0 +1,159 @@
+// Copyright (c) 2010 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 "src/device.h"
+
+#include <glog/logging.h>
+
+#include "src/service.h"
+
+namespace cashew {
+
+const char* Device::kCarrierUnknown = "Unknown";
+
+// Flimflam Device D-Bus identifiers
+static const char* kFlimflamDeviceName = "org.chromium.flimflam";
+
+// Flimflam Device property names
+static const char* kFlimflamDeviceCarrierProperty = "Cellular.Carrier";
+static const char* kFlimflamDeviceTypeProperty = "Type";
+
+// Flimflam Device on-the-wire Type values
+static const char* kFlimflamDeviceTypeEthernet = "ethernet";
+static const char* kFlimflamDeviceTypeWifi = "wifi";
+static const char* kFlimflamDeviceTypeWimax = "wimax";
+static const char* kFlimflamDeviceTypeBluetooth = "bluetooth";
+static const char* kFlimflamDeviceTypeGPS = "gps";
+static const char* kFlimflamDeviceTypeCellular = "cellular";
+// TODO(vlaviano): what does vendor look like?
+static const char* kFlimflamDeviceTypeVendor = "";
+
+Device::Device(DBus::Connection& connection,  // NOLINT
+               const DBus::Path& path)
+    : DBus::ObjectProxy(connection, path, kFlimflamDeviceName),
+      path_(path), type_(kTypeUnknown), carrier_(kCarrierUnknown) {
+  // init our state with a GetProperties() call to our Flimflam service path
+  // we'll update this state by monitoring PropertyChanged signals
+  GetDeviceProperties();
+}
+
+Device::~Device() {
+}
+
+const DBus::Path& Device::GetPath() const {
+  return path_;
+}
+
+Device::Type Device::GetType() const {
+  return type_;
+}
+
+const std::string& Device::GetCarrier() const {
+  return carrier_;
+}
+
+// Flimflam Device D-Bus Proxy methods
+void Device::PropertyChanged(const std::string& property_name,
+                             const DBus::Variant& new_value) {
+  DLOG(INFO) << path_ << ": PropertyChanged: property_name = " << property_name;
+  if (property_name.compare(kFlimflamDeviceCarrierProperty) == 0) {
+    OnCarrierUpdate(new_value.reader().get_string());
+  } else if (property_name.compare(kFlimflamDeviceTypeProperty) == 0) {
+    OnTypeUpdate(new_value.reader().get_string());
+  } else {
+    // we don't care about this property
+  }
+}
+
+// Private methods
+
+Device::Type Device::TypeFromString(const std::string& type) const {
+  if (type.compare(kFlimflamDeviceTypeEthernet) == 0) {
+    return kTypeEthernet;
+  }
+  if (type.compare(kFlimflamDeviceTypeWifi) == 0) {
+    return kTypeWifi;
+  }
+  if (type.compare(kFlimflamDeviceTypeWimax) == 0) {
+    return kTypeWimax;
+  }
+  if (type.compare(kFlimflamDeviceTypeBluetooth) == 0) {
+    return kTypeBluetooth;
+  }
+  if (type.compare(kFlimflamDeviceTypeGPS) == 0) {
+    return kTypeGPS;
+  }
+  if (type.compare(kFlimflamDeviceTypeCellular) == 0) {
+    return kTypeCellular;
+  }
+  if (type.compare(kFlimflamDeviceTypeVendor) == 0) {
+    return kTypeVendor;
+  }
+  return kTypeUnknown;
+}
+
+void Device::OnCarrierUpdate(const std::string& carrier) {
+  DLOG(INFO) << path_ << ": OnCarrierUpdate: carrier = " << carrier;
+  if (carrier_.compare(kCarrierUnknown) && carrier_.compare(carrier)) {
+    LOG(WARNING) << path_ << ": OnCarrierUpdate: carrier change from "
+        << carrier_ << " to " << carrier << "!";
+  }
+  // TODO(vlaviano): load backend module for this carrier if needed
+  carrier_ = carrier;
+}
+
+void Device::OnTypeUpdate(const std::string& type) {
+  DLOG(INFO) << path_ << ": OnTypeUpdate: type = " << type;
+  type_ = TypeFromString(type);
+  if (type_ != kTypeCellular) {
+    LOG(WARNING) << path_ << ": OnTypeUpdate: type is not cellular!";
+  }
+}
+
+void Device::GetDeviceProperties() {
+  DLOG(INFO) << path_ << ": GetDeviceProperties";
+  PropertyMap properties;
+  // dbus-c++ throws exceptions
+  // invoke the "Existing Non-conformant Code" clause of the style guide and
+  // isolate the rest of the system from this
+  try {
+    // TODO(vlaviano): make this call asynchronous
+    properties = GetProperties();
+  } catch (DBus::Error& error) {  // NOLINT
+    LOG(WARNING) << path_
+        << ": GetDeviceProperties: GetProperties() -> Exception: "
+        << error.name() << ": " << error.message();
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  } catch (...) {  // NOLINT
+    LOG(WARNING) << path_
+        << ": GetDeviceProperties: GetProperties() -> Exception";
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  }
+  DLOG(INFO) << "Received " << properties.size() << " properties";
+
+  // grab the properties in which we're interested
+  PropertyMap::const_iterator it;
+  it = properties.find(kFlimflamDeviceTypeProperty);
+  if (it != properties.end()) {
+    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
+    OnTypeUpdate(value.reader().get_string());
+  } else {
+    DLOG(WARNING) << path_ << ": GetDeviceProperties: no Type property";
+  }
+  // don't expect to find Cellular.* properties if we're not a cellular device
+  if (type_ != kTypeCellular) {
+    return;
+  }
+  it = properties.find(kFlimflamDeviceCarrierProperty);
+  if (it != properties.end()) {
+    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
+    OnCarrierUpdate(value.reader().get_string());
+  } else {
+    DLOG(WARNING) << path_ << ": GetDeviceProperties: no Carrier property";
+  }
+}
+
+}  // namespace cashew
diff --git a/src/device.h b/src/device.h
new file mode 100644
index 0000000..5fca3b6
--- /dev/null
+++ b/src/device.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2010 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 SRC_DEVICE_H_
+#define SRC_DEVICE_H_
+
+#include <string>
+
+#include <base/basictypes.h>  // NOLINT
+#include <dbus-c++/dbus.h>  // NOLINT
+
+#include "src/flimflam_device_client_glue.h"
+
+namespace cashew {
+
+// represents a cellular device and monitors Flimflam state for that device
+class Device : public org::chromium::flimflam::Device_proxy,
+               public DBus::IntrospectableProxy,
+               public DBus::ObjectProxy {
+  public:
+    Device(DBus::Connection& connection, const DBus::Path& path);  // NOLINT
+    virtual ~Device();
+
+    // get D-Bus path for this device
+    virtual const DBus::Path& GetPath() const;
+
+    // valid type values for this device
+    typedef enum {
+      kTypeUnknown = 0,
+      kTypeEthernet,
+      kTypeWifi,
+      kTypeWimax,
+      kTypeBluetooth,
+      kTypeGPS,
+      kTypeCellular,
+      kTypeVendor,
+    } Type;
+
+    // get type for this device
+    // NOTE: for now, should always be kTypeCellular
+    virtual Type GetType() const;
+
+    static const char* kCarrierUnknown;
+
+    // get cellular carrier for this device
+    // returns kCarrierUnknown if we don't know
+    virtual const std::string& GetCarrier() const;
+
+    // Flimflam Device D-Bus Proxy methods
+    virtual void PropertyChanged(const std::string& property_name,
+                                 const DBus::Variant& new_value);
+
+  private:
+    // D-Bus path for Flimflam device that we represent
+    // this is our unique identifier
+    const DBus::Path path_;
+
+    // device type
+    Type type_;
+
+    // cellular carrier
+    std::string carrier_;
+
+    // convert type string to Type enum value
+    Type TypeFromString(const std::string& type) const;
+
+    // we've received updated Cellular.Carrier info from Flimflam
+    void OnCarrierUpdate(const std::string& carrier);
+
+    // we've received updated Type info from Flimflam
+    void OnTypeUpdate(const std::string& type);
+
+    // get device properties from Flimflam
+    void GetDeviceProperties();
+
+    DISALLOW_COPY_AND_ASSIGN(Device);
+};
+
+}  // namespace cashew
+
+#endif  // SRC_DEVICE_H_
diff --git a/src/main.cc b/src/main.cc
index 279f954..852bca0 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -14,6 +14,7 @@
 #include <glog/logging.h>
 
 #include "src/cashew_server.h"
+#include "src/service_manager.h"
 
 namespace cashew {
 
@@ -35,6 +36,8 @@
 
 static CashewServer *server = NULL;
 
+static ServiceManager *service_manager = NULL;
+
 static void OnSignal(int signal) {
   DLOG(INFO) << "received signal " << signal;
   if (dispatcher) {
@@ -72,6 +75,8 @@
   DLOG(INFO) << "cleaning up";
   delete server;
   server = NULL;
+  delete service_manager;
+  service_manager = NULL;
   DBus::default_dispatcher = NULL;
   DBus::BusDispatcher *dispatcher_to_delete = dispatcher;
   dispatcher = NULL;
@@ -102,10 +107,22 @@
 
   // TODO(vlaviano): back-end carrier apis
 
+  DLOG(INFO) << "creating service manager";
+  DBus::Connection client_conn = DBus::Connection::SystemBus();
+  cashew::service_manager = new(std::nothrow) cashew::ServiceManager(
+      client_conn);
+  if (cashew::service_manager == NULL) {
+    LOG(ERROR) << "couldn't create service manager";
+    cashew::CleanUpForExit();
+    exit(EXIT_FAILURE);
+  }
+
   DLOG(INFO) << "creating cashew server";
-  DBus::Connection conn = DBus::Connection::SystemBus();
-  conn.request_name(cashew::CashewServer::kServiceName);
-  cashew::server = new(std::nothrow) cashew::CashewServer(conn);
+  // TODO(vlaviano): can we share a single conn for client and server roles?
+  DBus::Connection server_conn = DBus::Connection::SystemBus();
+  server_conn.request_name(cashew::CashewServer::kServiceName);
+  cashew::server = new(std::nothrow) cashew::CashewServer(server_conn,
+      *cashew::service_manager);
   if (cashew::server == NULL) {
     LOG(ERROR) << "couldn't create cashew server";
     cashew::CleanUpForExit();
diff --git a/src/service.cc b/src/service.cc
new file mode 100644
index 0000000..a05f431
--- /dev/null
+++ b/src/service.cc
@@ -0,0 +1,277 @@
+// Copyright (c) 2010 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 "src/service.h"
+
+#include <glog/logging.h>
+
+#include "src/device.h"
+
+namespace cashew {
+
+// Flimflam Service D-Bus identifiers
+static const char* kFlimflamServiceName = "org.chromium.flimflam";
+
+// Flimflam Service property names
+static const char *kFlimflamServiceDeviceProperty = "Device";
+static const char *kFlimflamServiceStateProperty = "State";
+static const char* kFlimflamServiceTypeProperty = "Type";
+
+// Flimflam Service on-the-wire State values
+static const char* kFlimflamServiceStateIdle = "idle";
+static const char* kFlimflamServiceStateCarrier = "carrier";
+static const char* kFlimflamServiceStateAssociation = "association";
+static const char* kFlimflamServiceStateConfiguration = "configuration";
+static const char* kFlimflamServiceStateReady = "ready";
+static const char* kFlimflamServiceStateDisconnect = "disconnect";
+static const char* kFlimflamServiceStateFailure = "failure";
+static const char* kFlimflamServiceStateActivationFailure =
+    "activation-failure";
+
+// Flimflam Service on-the-wire Type values
+static const char* kFlimflamServiceTypeEthernet = "ethernet";
+static const char* kFlimflamServiceTypeWifi = "wifi";
+static const char* kFlimflamServiceTypeWimax = "wimax";
+static const char* kFlimflamServiceTypeBluetooth = "bluetooth";
+static const char* kFlimflamServiceTypeCellular = "cellular";
+
+Service::Service(DBus::Connection& connection,  // NOLINT
+                 const DBus::Path& path)
+    : DBus::ObjectProxy(connection, path, kFlimflamServiceName),
+      connection_(connection), path_(path), state_(kStateUnknown),
+      type_(kTypeUnknown), device_(NULL) {
+  // init our state with a GetProperties() call to our Flimflam service path
+  // we'll update this state by monitoring PropertyChanged signals
+  GetServiceProperties();
+
+  // init our data plans list with a hardcoded plan
+  // TODO(vlaviano): implement backend and return real data
+  AddHardcodedDataPlan();
+}
+
+Service::~Service() {
+  DeleteDataPlans();
+  if (device_ != NULL) {
+    DLOG(INFO) << path_ << ": deleting device " << device_->GetPath();
+    delete device_;
+    device_ = NULL;
+  }
+}
+
+const DBus::Path& Service::GetPath() const {
+  return path_;
+}
+
+Service::State Service::GetState() const {
+  return state_;
+}
+
+Service::Type Service::GetType() const {
+  return type_;
+}
+
+Device* Service::GetDevice() const {
+  return device_;
+}
+
+DBusDataPlanList Service::GetDBusDataPlans() const {
+  DBusDataPlanList dbus_data_plans;
+  DataPlanList::const_iterator it;
+  for (it = data_plans_.begin(); it != data_plans_.end(); ++it) {
+    DataPlan *plan = *it;
+    DCHECK(plan != NULL);
+    dbus_data_plans.push_back(plan->ToDBusFormat());
+  }
+  return dbus_data_plans;
+}
+
+// Flimflam Service D-Bus Proxy methods
+
+void Service::PropertyChanged(const std::string& property_name,
+                              const DBus::Variant& new_value) {
+  DLOG(INFO) << path_ << ": PropertyChanged: property_name = " << property_name;
+  if (property_name.compare(kFlimflamServiceDeviceProperty) == 0) {
+    OnDeviceUpdate(new_value.reader().get_path());
+  } else if (property_name.compare(kFlimflamServiceStateProperty) == 0) {
+    OnStateUpdate(new_value.reader().get_string());
+  } else if (property_name.compare(kFlimflamServiceTypeProperty) == 0) {
+    OnTypeUpdate(new_value.reader().get_string());
+  } else {
+    // we don't care about this property
+  }
+}
+
+// Private methods
+
+Service::State Service::StateFromString(const std::string& state) const {
+  if (state.compare(kFlimflamServiceStateIdle) == 0) {
+    return kStateIdle;
+  }
+  if (state.compare(kFlimflamServiceStateCarrier) == 0) {
+    return kStateCarrier;
+  }
+  if (state.compare(kFlimflamServiceStateAssociation) == 0) {
+    return kStateAssociation;
+  }
+  if (state.compare(kFlimflamServiceStateConfiguration) == 0) {
+    return kStateConfiguration;
+  }
+  if (state.compare(kFlimflamServiceStateReady) == 0) {
+    return kStateReady;
+  }
+  if (state.compare(kFlimflamServiceStateDisconnect) == 0) {
+    return kStateDisconnect;
+  }
+  if (state.compare(kFlimflamServiceStateFailure) == 0) {
+    return kStateFailure;
+  }
+  if (state.compare(kFlimflamServiceStateActivationFailure) == 0) {
+    return kStateActivationFailure;
+  }
+  return kStateUnknown;
+}
+
+Service::Type Service::TypeFromString(const std::string& type) const {
+  if (type.compare(kFlimflamServiceTypeEthernet) == 0) {
+    return kTypeEthernet;
+  }
+  if (type.compare(kFlimflamServiceTypeWifi) == 0) {
+    return kTypeWifi;
+  }
+  if (type.compare(kFlimflamServiceTypeWimax) == 0) {
+    return kTypeWimax;
+  }
+  if (type.compare(kFlimflamServiceTypeBluetooth) == 0) {
+    return kTypeBluetooth;
+  }
+  if (type.compare(kFlimflamServiceTypeCellular) == 0) {
+    return kTypeCellular;
+  }
+  return kTypeUnknown;
+}
+
+void Service::OnDeviceUpdate(const DBus::Path& device_path) {
+  DLOG(INFO) << path_ << ": OnDeviceUpdate: device_path = " << device_path;
+
+  // if there's an existing device with a non-matching path: destroy it and
+  // fall through to make a new one below.
+  if (device_ != NULL && device_->GetPath().compare(device_path)) {
+    LOG(WARNING) << path_ << ": OnDeviceUpdate: device path changed from "
+        << device_->GetPath() << " to " << device_path;
+    delete device_;
+    device_ = NULL;
+  }
+
+  // if there's an existing device with matching path: all is well
+  if (device_ != NULL) {
+    return;
+  }
+
+  // if there's no existing device: make one
+  device_ = new(std::nothrow) Device(connection_, device_path);
+  if (device_ == NULL) {
+    LOG(ERROR) << path_ << ": OnDeviceUpdate: couldn't create device for "
+        << device_path;
+    return;
+  }
+  DLOG(INFO) << path_ << ": OnDeviceUpdate: created device for "
+      << device_path;
+}
+
+void Service::OnStateUpdate(const std::string& state) {
+  DLOG(INFO) << path_ << ": OnStateUpdate: state = " << state;
+  state_ = StateFromString(state);
+  // TODO(vlaviano): react to state changes
+  // note that ready does not necessarily mean that we're the default route
+}
+
+void Service::OnTypeUpdate(const std::string& type) {
+  DLOG(INFO) << path_ << ": OnTypeUpdate: type = " << type;
+  type_ = TypeFromString(type);
+  if (type_ != kTypeCellular) {
+    LOG(WARNING) << path_ << ": OnTypeUpdate: type is not cellular!";
+  }
+}
+
+void Service::GetServiceProperties() {
+  DLOG(INFO) << path_ << ": GetServiceProperties";
+  PropertyMap properties;
+  // dbus-c++ throws exceptions
+  // invoke the "Existing Non-conformant Code" clause of the style guide and
+  // isolate the rest of the system from this
+  try {
+    // TODO(vlaviano): make this call asynchronous
+    properties = GetProperties();
+  } catch (DBus::Error& error) {  // NOLINT
+    LOG(WARNING) << path_
+        << ": GetServiceProperties: GetProperties() -> Exception: "
+        << error.name() << ": " << error.message();
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  } catch (...) {  // NOLINT
+    LOG(WARNING) << path_
+        << ": GetServiceProperties: GetProperties() -> Exception";
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  }
+  DLOG(INFO) << "Received " << properties.size() << " properties";
+
+  // grab the properties in which we're interested
+  PropertyMap::const_iterator it;
+  it = properties.find(kFlimflamServiceDeviceProperty);
+  if (it != properties.end()) {
+    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
+    OnDeviceUpdate(value.reader().get_path());
+  } else {
+    DLOG(WARNING) << path_ << ": GetServiceProperties: no Device property";
+  }
+  it = properties.find(kFlimflamServiceStateProperty);
+  if (it != properties.end()) {
+    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
+    OnStateUpdate(value.reader().get_string());
+  } else {
+    DLOG(WARNING) << path_ << ": GetServiceProperties: no State property";
+  }
+  it = properties.find(kFlimflamServiceTypeProperty);
+  if (it != properties.end()) {
+    const DBus::Variant& value = static_cast<DBus::Variant>(it->second);
+    OnTypeUpdate(value.reader().get_string());
+  } else {
+    DLOG(WARNING) << path_ << ": GetServiceProperties: no Type property";
+  }
+}
+
+void Service::DeleteDataPlans() {
+  while (!data_plans_.empty()) {
+    DataPlan *plan = *data_plans_.begin();
+    DCHECK(plan != NULL);
+    DLOG(INFO) << path_ << ": DeleteDataPlans: deleting plan: "
+        << plan->GetName();
+    delete plan;
+    data_plans_.erase(data_plans_.begin());
+  }
+}
+
+void Service::AddHardcodedDataPlan() {
+  const std::string name = "Chromium OS Test Plan";
+  DataPlan::Type type = DataPlan::kTypeMeteredPaid;
+  base::Time update_time = base::Time::Now();
+  base::TimeDelta twelve_hours = base::TimeDelta::FromHours(12);
+  base::Time start_time = base::Time::Now() - twelve_hours;
+  base::Time end_time = base::Time::Now() + twelve_hours;
+  Bytes data_bytes_max = 100*1024*1024;  // 100 MB
+  Bytes data_bytes_used = 30*1024*1024;  // 30 MB
+
+  DataPlan *plan = new(std::nothrow) DataPlan(name, type, update_time,
+                                              start_time, end_time,
+                                              data_bytes_max, data_bytes_used);
+  if (plan == NULL) {
+    LOG(ERROR) << path_ << ": AddHardcodedDataPlan: could not create plan";
+    return;
+  }
+
+  data_plans_.push_back(plan);
+}
+
+}  // namespace cashew
diff --git a/src/service.h b/src/service.h
new file mode 100644
index 0000000..8edf613
--- /dev/null
+++ b/src/service.h
@@ -0,0 +1,132 @@
+// Copyright (c) 2010 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 SRC_SERVICE_H_
+#define SRC_SERVICE_H_
+
+#include <map>
+#include <string>
+
+#include <base/basictypes.h>  // NOLINT
+#include <dbus-c++/dbus.h>    // NOLINT
+
+#include "src/data_plan.h"
+#include "src/flimflam_service_client_glue.h"
+
+namespace cashew {
+
+class Device;
+
+// map of key, value pairs representing service properties received via D-Bus
+typedef std::map<std::string, DBus::Variant> PropertyMap;
+
+// represents a cellular service and monitors Flimflam state for that service
+class Service : public org::chromium::flimflam::Service_proxy,
+                public DBus::IntrospectableProxy,
+                public DBus::ObjectProxy {
+  public:
+    Service(DBus::Connection& connection, const DBus::Path& path);  // NOLINT
+    virtual ~Service();
+
+    // get D-Bus path for this service
+    virtual const DBus::Path& GetPath() const;
+
+    // valid state values for this service
+    typedef enum {
+      kStateUnknown = 0,
+      kStateIdle,
+      kStateCarrier,
+      kStateAssociation,
+      kStateConfiguration,
+      kStateReady,
+      kStateDisconnect,
+      kStateFailure,
+      kStateActivationFailure,
+    } State;
+
+    // get state for this service
+    virtual State GetState() const;
+
+    // valid type values for this service
+    typedef enum {
+      kTypeUnknown = 0,
+      kTypeEthernet,
+      kTypeWifi,
+      kTypeWimax,
+      kTypeBluetooth,
+      kTypeCellular,
+    } Type;
+
+    // get type for this service
+    // NOTE: for now, should always be kTypeCellular
+    virtual Type GetType() const;
+
+    // get Device object for this service
+    // this can return NULL if we don't yet have Device info
+    virtual Device* GetDevice() const;
+
+    // get data plan info, formatted for D-Bus
+    virtual DBusDataPlanList GetDBusDataPlans() const;
+
+    // Flimflam Service D-Bus Proxy methods
+
+    // receive incoming PropertyChanged D-Bus signal from Flimflam service
+    virtual void PropertyChanged(const std::string& property_name,
+                                 const DBus::Variant& new_value);
+
+  private:
+    // D-Bus connection, owned by our creator
+    // shared with Device obj that we create
+    DBus::Connection& connection_;
+
+    // D-Bus path for Flimflam service that we represent
+    // this is our unique identifier
+    const DBus::Path path_;
+
+    // Service connection state
+    State state_;
+
+    // Service type
+    Type type_;
+
+    // Device corresponding to our service
+    Device *device_;
+
+    // cached data plan info
+    DataPlanList data_plans_;
+
+    // TODO(vlaviano): reference to back end carrier plugin
+    // TODO(vlaviano): refresh data plan info from carrier plugin
+
+    // convert state string to State enum value
+    State StateFromString(const std::string& state) const;
+
+    // convert type string to Type enum value
+    Type TypeFromString(const std::string& type) const;
+
+    // we've received updated Device info from Flimflam
+    void OnDeviceUpdate(const DBus::Path& device_path);
+
+    // we've received updated State info from Flimflam
+    void OnStateUpdate(const std::string& state);
+
+    // we've received updated Type info from Flimflam
+    void OnTypeUpdate(const std::string& type);
+
+    // get service properties from Flimflam
+    void GetServiceProperties();
+
+    // clear data_plans_ list and delete data plan objects
+    void DeleteDataPlans();
+
+    // create a hardcoded data plan and add it to data_plans_ list
+    // for debugging
+    void AddHardcodedDataPlan();
+
+    DISALLOW_COPY_AND_ASSIGN(Service);
+};
+
+}  // namespace cashew
+
+#endif  // SRC_SERVICE_H_
diff --git a/src/service_manager.cc b/src/service_manager.cc
new file mode 100644
index 0000000..39f3c6d
--- /dev/null
+++ b/src/service_manager.cc
@@ -0,0 +1,171 @@
+// Copyright (c) 2010 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 "src/service_manager.h"
+
+#include <glog/logging.h>
+
+#include "src/service.h"
+
+namespace cashew {
+
+// Flimflam D-Bus indentifiers
+static const char* kFlimflamManagerName = "org.chromium.flimflam";
+static const char* kFlimflamManagerPath = "/";
+
+// Flimflam property names
+static const char* kServicesProperty = "Services";
+
+// Flimflam service types
+static const char* kTypeCellular = "cellular";
+
+ServiceManager::ServiceManager(DBus::Connection& connection)  // NOLINT
+    : DBus::ObjectProxy(connection, kFlimflamManagerPath,
+                        kFlimflamManagerName),
+      connection_(connection) {
+  // init our state with a GetProperties() call to Flimflam
+  // we'll update this state by monitoring PropertyChanged signals
+  GetFlimflamServices();
+}
+
+ServiceManager::~ServiceManager() {
+  DeleteServices(&services_);
+}
+
+const Service* ServiceManager::GetService(const std::string& service_path)
+    const {
+  ServiceMap::const_iterator it = services_.find(service_path);
+  if (it == services_.end()) {
+    return NULL;
+  }
+  DCHECK(it->second != NULL);
+  return static_cast<const Service *>(it->second);
+}
+
+// Flimflam Manager D-Bus Proxy methods
+
+void ServiceManager::PropertyChanged(const std::string& property_name,
+                                     const DBus::Variant& new_value) {
+  DLOG(INFO) << "PropertyChanged: property_name = " << property_name;
+
+  // we only care about Services changes
+  if (property_name.compare(kServicesProperty)) {
+      return;
+  }
+
+  // interpret new_value as a vector of service paths
+  ServicePathList paths;
+  DBus::MessageIter reader = new_value.reader();
+  reader >> paths;
+
+  OnServicesUpdate(paths);
+}
+
+void ServiceManager::StateChanged(const std::string& new_state) {
+  DLOG(INFO) << "StateChanged: new_state = " << new_state;
+  // TODO(vlaviano): switch on state and call private onFlimflamOffline(),
+  // onFlimflamOnline() hooks.  If we care.
+}
+
+// Private methods
+
+void ServiceManager::DeleteServices(ServiceMap *service_map) {
+  DCHECK(service_map != NULL);
+  while (!service_map->empty()) {
+    const std::string& path =
+        static_cast<std::string>(service_map->begin()->first);
+    DLOG(INFO) << "DeleteServices: deleting service: " << path;
+    Service *service = static_cast<Service *>(service_map->begin()->second);
+    delete service;
+    service_map->erase(service_map->begin());
+  }
+}
+
+void ServiceManager::GetFlimflamServices() {
+  DLOG(INFO) << "GetFlimflamServices";
+  PropertyMap properties;
+  // dbus-c++ throws exceptions
+  // invoke the "Existing Non-conformant Code" clause of the style guide and
+  // isolate the rest of the system from this
+  try {
+    // TODO(vlaviano): make this call asynchronous
+    properties = GetProperties();
+  } catch (DBus::Error& error) {  // NOLINT
+    LOG(WARNING) << "GetFlimflamServices: GetProperties() -> Exception: "
+        << error.name() << ": " << error.message();
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  } catch (...) {  // NOLINT
+    LOG(WARNING) << "GetFlimflamServices: GetProperties() -> Exception";
+    // TODO(vlaviano): schedule another attempt later
+    return;
+  }
+
+  // grab Services property from result
+  PropertyMap::const_iterator it = properties.find(kServicesProperty);
+  if (it == properties.end()) {
+    DLOG(WARNING) << "GetFlimflamServices: no Services property";
+    return;
+  }
+  DBus::Variant value = static_cast<DBus::Variant>(it->second);
+
+  // interpret value as vector of service paths
+  ServicePathList paths;
+  DBus::MessageIter reader = value.reader();
+  reader >> paths;
+
+  OnServicesUpdate(paths);
+}
+
+void ServiceManager::OnServicesUpdate(const ServicePathList& paths) {
+  // Remember old set of services. This is the baseline from which we will
+  // determine what has changed.
+  ServiceMap old_services = services_;
+  services_.clear();
+
+  // Flimflam service types are embedded in their path names, so we can use
+  // the pattern "/cellular_" to decide which paths are cellular services.
+  static const std::string cellular_pattern = std::string("/") + kTypeCellular
+      + "_";
+
+  // Walk paths in this update and see if they're new to us
+  DLOG(INFO) << "OnServicesUpdate: " << paths.size() << " service paths";
+  ServicePathList::const_iterator it;
+  for (it = paths.begin(); it != paths.end(); ++it) {
+    const DBus::Path& path = *it;
+
+    // We only care about cellular services
+    if (path.find(cellular_pattern) == std::string::npos) {
+      continue;
+    }
+
+    ServiceMap::iterator old_it = old_services.find(path);
+    if (old_it != old_services.end()) {
+      // service is old: move it from old_services to services_
+      Service *service = static_cast<Service *>(old_it->second);
+      DCHECK(service != NULL);
+      old_services.erase(old_it);
+      services_[path] = service;
+    } else {
+      // service is new: create a Service obj for it and add it to services_
+      OnNewService(path);
+    }
+  }
+
+  // services left in old_services failed to appear in the update: delete them
+  DeleteServices(&old_services);
+}
+
+void ServiceManager::OnNewService(const DBus::Path& path) {
+  DLOG(INFO) << "OnNewService: " << path;
+  DCHECK(GetService(path) == NULL);
+  Service *service = new(std::nothrow) Service(connection_, path);
+  if (service == NULL) {
+    LOG(ERROR) << "OnNewService: couldn't create service for " << path;
+    return;
+  }
+  services_[path] = service;
+}
+
+}  // namespace cashew
diff --git a/src/service_manager.h b/src/service_manager.h
new file mode 100644
index 0000000..4eae79e
--- /dev/null
+++ b/src/service_manager.h
@@ -0,0 +1,73 @@
+// Copyright (c) 2010 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 SRC_SERVICE_MANAGER_H_
+#define SRC_SERVICE_MANAGER_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <base/basictypes.h>  // NOLINT
+#include <dbus-c++/dbus.h>  // NOLINT
+
+#include "src/flimflam_manager_client_glue.h"
+
+namespace cashew {
+
+class Service;
+
+// map of service path names to Service objects
+typedef std::map<std::string, Service*> ServiceMap;
+
+// vector of DBus::Path objects (strings) representing service path names
+typedef std::vector<DBus::Path> ServicePathList;
+
+// monitors Flimflam and maintains a collection of cellular service objects
+class ServiceManager : public org::chromium::flimflam::Manager_proxy,
+                       public DBus::IntrospectableProxy,
+                       public DBus::ObjectProxy {
+  public:
+    explicit ServiceManager(DBus::Connection& connection);  // NOLINT
+    virtual ~ServiceManager();
+
+    // look up a service by its path name
+    // returns NULL if service is not found
+    virtual const Service* GetService(const std::string& service_path) const;
+
+    // Flimflam Manager D-Bus Proxy methods
+
+    // receive incoming PropertyChanged D-Bus signal from Flimflam
+    virtual void PropertyChanged(const std::string& property_name,
+                                 const DBus::Variant& new_value);
+
+    // receive incoming StateChanged D-Bus signal from Flimflam
+    virtual void StateChanged(const std::string& new_state);
+
+  private:
+    // D-Bus connection owned by our creator
+    // shared with Service objs that we create
+    DBus::Connection& connection_;
+
+    // collection of Service objs representing cellular services
+    ServiceMap services_;
+
+    // delete a collection of services
+    void DeleteServices(ServiceMap *service_map);
+
+    // get Services info from Flimflam
+    void GetFlimflamServices();
+
+    // we've received updated Services info from Flimflam
+    void OnServicesUpdate(const ServicePathList& paths);
+
+    // Flimflam has started advertising a new service
+    void OnNewService(const DBus::Path& path);
+
+    DISALLOW_COPY_AND_ASSIGN(ServiceManager);
+};
+
+}  // namespace cashew
+
+#endif  // SRC_SERVICE_MANAGER_H_