shill: Make cellular Connect work for GSM networks.

Add the APN to the properties map passed to Connect.
Set up the list of APNs to try, and step through it when
a connect attempt fails with the InvalidApn error. The
order in which APNs are tried is

  - APN which most recently resulted in a successful
    connection on the current network
  - a user-specified APN, specified by setting the Cellular.APN
    property on the service
  - the list of APNs found for the home network provider in the
    mobile broadband provider database
  - if all those fail, a null APN

BUG=chromium-os:23259
TEST=manual testing, some of which involved modifying the
mobile provider DB to create invalid APN entries. Created
two new unit tests as well, and ran all unit tests.

Change-Id: I38c869228fe1aaf7421de8a826c54e7b62c209b2
Reviewed-on: https://gerrit.chromium.org/gerrit/19122
Tested-by: Eric Shienbrood <ers@chromium.org>
Reviewed-by: Jason Glasgow <jglasgow@chromium.org>
Commit-Ready: Eric Shienbrood <ers@chromium.org>
diff --git a/cellular.cc b/cellular.cc
index bf49d2f..3410645 100644
--- a/cellular.cc
+++ b/cellular.cc
@@ -301,8 +301,7 @@
 
 void Cellular::Connect(Error *error) {
   VLOG(2) << __func__;
-  if (state_ == kStateConnected ||
-      state_ == kStateLinked) {
+  if (state_ == kStateConnected || state_ == kStateLinked) {
     Error::PopulateAndLog(error, Error::kAlreadyConnected,
                           "Already connected; connection request ignored.");
     return;
@@ -333,8 +332,8 @@
   }
 }
 
-void Cellular::OnConnectFailed() {
-  // TODO(ers): Signal failure.
+void Cellular::OnConnectFailed(const Error &error) {
+  service()->SetFailure(Service::kFailureUnknown);
 }
 
 void Cellular::Disconnect(Error *error) {
diff --git a/cellular.h b/cellular.h
index 8d115c8..cbe2197 100644
--- a/cellular.h
+++ b/cellular.h
@@ -167,7 +167,7 @@
   void OnModemStopped(const EnabledStateChangedCallback &callback,
                       const Error &error);
   void OnConnected();
-  void OnConnectFailed();
+  void OnConnectFailed(const Error &error);
   void OnDisconnected();
   void OnDisconnectFailed();
 
diff --git a/cellular_capability.cc b/cellular_capability.cc
index 4dd3cd9..10a068e 100644
--- a/cellular_capability.cc
+++ b/cellular_capability.cc
@@ -19,6 +19,10 @@
 
 namespace shill {
 
+const char CellularCapability::kConnectPropertyApn[] = "apn";
+const char CellularCapability::kConnectPropertyApnUsername[] = "username";
+const char CellularCapability::kConnectPropertyApnPassword[] = "password";
+const char CellularCapability::kConnectPropertyHomeOnly[] = "home_only";
 const char CellularCapability::kConnectPropertyPhoneNumber[] = "number";
 const char CellularCapability::kPropertyIMSI[] = "imsi";
 // All timeout values are in milliseconds
@@ -292,7 +296,7 @@
   if (error.IsSuccess())
     cellular()->OnConnected();
   else
-    cellular()->OnConnectFailed();
+    cellular()->OnConnectFailed(error);
   if (!callback.is_null())
     callback.Run(error);
 }
diff --git a/cellular_capability.h b/cellular_capability.h
index cde66eb..105e119 100644
--- a/cellular_capability.h
+++ b/cellular_capability.h
@@ -38,6 +38,10 @@
   static const int kTimeoutRegister;
   static const int kTimeoutScan;
 
+  static const char kConnectPropertyApn[];
+  static const char kConnectPropertyApnUsername[];
+  static const char kConnectPropertyApnPassword[];
+  static const char kConnectPropertyHomeOnly[];
   static const char kConnectPropertyPhoneNumber[];
   static const char kPropertyIMSI[];
 
@@ -134,6 +138,8 @@
 
   static void OnUnsupportedOperation(const char *operation, Error *error);
 
+  virtual void OnConnectReply(const ResultCallback &callback,
+                              const Error &error);
   // Run the next task in a list.
   // Precondition: |tasks| is not empty.
   void RunNextStep(CellularTaskList *tasks);
@@ -165,6 +171,7 @@
   FRIEND_TEST(CellularCapabilityTest, FinishEnable);
   FRIEND_TEST(CellularCapabilityTest, GetModemInfo);
   FRIEND_TEST(CellularCapabilityTest, GetModemStatus);
+  FRIEND_TEST(CellularCapabilityTest, TryApns);
   FRIEND_TEST(CellularServiceTest, FriendlyName);
   FRIEND_TEST(CellularTest, StartCDMARegister);
   FRIEND_TEST(CellularTest, StartConnected);
@@ -192,8 +199,6 @@
   virtual void OnGetModemStatusReply(const ResultCallback &callback,
                                      const DBusPropertiesMap &props,
                                      const Error &error);
-  virtual void OnConnectReply(const ResultCallback &callback,
-                              const Error &error);
   virtual void OnDisconnectReply(const ResultCallback &callback,
                                  const Error &error);
 
diff --git a/cellular_capability_gsm.cc b/cellular_capability_gsm.cc
index 6a7e88e..51f80f3 100644
--- a/cellular_capability_gsm.cc
+++ b/cellular_capability_gsm.cc
@@ -184,11 +184,81 @@
   }
 }
 
+// Create the list of APNs to try, in the following order:
+// - last APN that resulted in a successful connection attempt on the
+//   current network (if any)
+// - the APN, if any, that was set by the user
+// - the list of APNs found in the mobile broadband provider DB for the
+//   home provider associated with the current SIM
+// - as a last resort, attempt to connect with no APN
+void CellularCapabilityGSM::SetupApnTryList() {
+  apn_try_list_.clear();
+
+  DCHECK(cellular()->service().get());
+  const Stringmap *apn_info = cellular()->service()->GetLastGoodApn();
+  if (apn_info)
+    apn_try_list_.push_back(*apn_info);
+
+  apn_info = cellular()->service()->GetUserSpecifiedApn();
+  if (apn_info)
+    apn_try_list_.push_back(*apn_info);
+
+  apn_try_list_.insert(apn_try_list_.end(), apn_list_.begin(), apn_list_.end());
+}
+
 void CellularCapabilityGSM::SetupConnectProperties(
     DBusPropertiesMap *properties) {
+  SetupApnTryList();
+  FillConnectPropertyMap(properties);
+}
+
+void CellularCapabilityGSM::FillConnectPropertyMap(
+    DBusPropertiesMap *properties) {
   (*properties)[kConnectPropertyPhoneNumber].writer().append_string(
       kPhoneNumber);
-  // TODO(petkov): Setup apn and "home_only".
+
+  if (!allow_roaming_)
+    (*properties)[kConnectPropertyHomeOnly].writer().append_bool(true);
+
+  if (!apn_try_list_.empty()) {
+    // Leave the APN at the front of the list, so that it can be recorded
+    // if the connect attempt succeeds.
+    Stringmap apn_info = apn_try_list_.front();
+    VLOG(2) << __func__ << ": Using APN " << apn_info[flimflam::kApnProperty];
+    (*properties)[kConnectPropertyApn].writer().append_string(
+        apn_info[flimflam::kApnProperty].c_str());
+    if (ContainsKey(apn_info, flimflam::kApnUsernameProperty))
+      (*properties)[kConnectPropertyApnUsername].writer().append_string(
+          apn_info[flimflam::kApnUsernameProperty].c_str());
+    if (ContainsKey(apn_info, flimflam::kApnPasswordProperty))
+      (*properties)[kConnectPropertyApnPassword].writer().append_string(
+          apn_info[flimflam::kApnPasswordProperty].c_str());
+  }
+}
+
+void CellularCapabilityGSM::OnConnectReply(const ResultCallback &callback,
+                                           const Error &error) {
+  if (error.IsFailure()) {
+    cellular()->service()->ClearLastGoodApn();
+    // The APN that was just tried (and failed) is still at the
+    // front of the list, about to be removed. If the list is empty
+    // after that, try one last time without an APN. This may succeed
+    // with some modems in some cases.
+    if (error.type() == Error::kInvalidApn && !apn_try_list_.empty()) {
+      apn_try_list_.pop_front();
+      VLOG(2) << "Connect failed with invalid APN, " << apn_try_list_.size()
+              << " remaining APNs to try";
+      DBusPropertiesMap props;
+      FillConnectPropertyMap(&props);
+      Error error;
+      Connect(props, &error, callback);
+      return;
+    }
+  } else if (!apn_try_list_.empty()) {
+    cellular()->service()->SetLastGoodApn(apn_try_list_.front());
+    apn_try_list_.clear();
+  }
+  CellularCapability::OnConnectReply(callback, error);
 }
 
 // always called from an async context
diff --git a/cellular_capability_gsm.h b/cellular_capability_gsm.h
index 916506b..2f22628 100644
--- a/cellular_capability_gsm.h
+++ b/cellular_capability_gsm.h
@@ -5,6 +5,8 @@
 #ifndef SHILL_CELLULAR_CAPABILITY_GSM_
 #define SHILL_CELLULAR_CAPABILITY_GSM_
 
+#include <deque>
+
 #include <base/memory/scoped_ptr.h>
 #include <base/memory/weak_ptr.h>
 #include <gtest/gtest_prod.h>  // for FRIEND_TEST
@@ -68,6 +70,10 @@
  protected:
   virtual void InitProxies();
   virtual void ReleaseProxies();
+  // Override OnConnectReply in order to handle the possibility of
+  // retrying the Connect operation.
+  virtual void OnConnectReply(const ResultCallback &callback,
+                              const Error &error);
 
  private:
   friend class CellularTest;
@@ -93,7 +99,9 @@
   FRIEND_TEST(CellularCapabilityGSMTest, UpdateOperatorInfo);
   FRIEND_TEST(CellularCapabilityGSMTest, GetRegistrationState);
   FRIEND_TEST(CellularCapabilityGSMTest, OnModemManagerPropertiesChanged);
+  FRIEND_TEST(CellularCapabilityGSMTest, SetupApnTryList);
   FRIEND_TEST(CellularCapabilityTest, AllowRoaming);
+  FRIEND_TEST(CellularCapabilityTest, TryApns);
   FRIEND_TEST(CellularTest, StartGSMRegister);
   FRIEND_TEST(ModemTest, CreateDeviceFromProperties);
 
@@ -138,6 +146,9 @@
 
   KeyValueStore SimLockStatusToProperty(Error *error);
 
+  void SetupApnTryList();
+  void FillConnectPropertyMap(DBusPropertiesMap *properties);
+
   void HelpRegisterDerivedKeyValueStore(
       const std::string &name,
       KeyValueStore(CellularCapabilityGSM::*get)(Error *error),
@@ -189,6 +200,7 @@
   // Properties.
   std::string selected_network_;
   Stringmaps found_networks_;
+  std::deque<Stringmap> apn_try_list_;
   bool scanning_;
   uint16 scan_interval_;
   SimLockStatus sim_lock_status_;
diff --git a/cellular_capability_gsm_unittest.cc b/cellular_capability_gsm_unittest.cc
index 5083736..c516ee9 100644
--- a/cellular_capability_gsm_unittest.cc
+++ b/cellular_capability_gsm_unittest.cc
@@ -18,6 +18,7 @@
 #include "shill/mock_metrics.h"
 #include "shill/mock_modem_gsm_card_proxy.h"
 #include "shill/mock_modem_gsm_network_proxy.h"
+#include "shill/mock_profile.h"
 #include "shill/nice_mock_control.h"
 
 using base::Bind;
@@ -617,4 +618,71 @@
   EXPECT_EQ(kRetries, capability_->sim_lock_status_.retries_left);
 }
 
+TEST_F(CellularCapabilityGSMTest, SetupApnTryList) {
+  static const string kTmobileApn("epc.tmobile.com");
+  static const string kLastGoodApn("remembered.apn");
+  static const string kLastGoodUsername("remembered.user");
+  static const string kSuppliedApn("my.apn");
+
+  SetService();
+  capability_->imsi_ = "310240123456789";
+  InitProviderDB();
+  capability_->SetHomeProvider();
+  DBusPropertiesMap props;
+  capability_->SetupConnectProperties(&props);
+  EXPECT_FALSE(props.find(flimflam::kApnProperty) == props.end());
+  EXPECT_EQ(kTmobileApn, props[flimflam::kApnProperty].reader().get_string());
+
+  ProfileRefPtr profile(new NiceMock<MockProfile>(
+      &control_, reinterpret_cast<Manager *>(NULL)));
+  cellular_->service()->set_profile(profile);
+  Stringmap apn_info;
+  apn_info[flimflam::kApnProperty] = kLastGoodApn;
+  apn_info[flimflam::kApnUsernameProperty] = kLastGoodUsername;
+  cellular_->service()->SetLastGoodApn(apn_info);
+  props.clear();
+  EXPECT_TRUE(props.find(flimflam::kApnProperty) == props.end());
+  capability_->SetupConnectProperties(&props);
+  // We expect the list to contain the last good APN, plus
+  // the 4 APNs from the mobile provider info database.
+  EXPECT_EQ(5, capability_->apn_try_list_.size());
+  EXPECT_FALSE(props.find(flimflam::kApnProperty) == props.end());
+  EXPECT_EQ(kLastGoodApn, props[flimflam::kApnProperty].reader().get_string());
+  EXPECT_FALSE(props.find(flimflam::kApnUsernameProperty) == props.end());
+  EXPECT_EQ(kLastGoodUsername,
+            props[flimflam::kApnUsernameProperty].reader().get_string());
+
+  Error error;
+  apn_info.clear();
+  props.clear();
+  apn_info[flimflam::kApnProperty] = kSuppliedApn;
+  // Setting the APN has the side effect of clearing the LastGoodApn,
+  // so the try list will have 5 elements, with the first one being
+  // the supplied APN.
+  cellular_->service()->SetApn(apn_info, &error);
+  EXPECT_TRUE(props.find(flimflam::kApnProperty) == props.end());
+  capability_->SetupConnectProperties(&props);
+  EXPECT_EQ(5, capability_->apn_try_list_.size());
+  EXPECT_FALSE(props.find(flimflam::kApnProperty) == props.end());
+  EXPECT_EQ(kSuppliedApn, props[flimflam::kApnProperty].reader().get_string());
+
+  apn_info.clear();
+  props.clear();
+  apn_info[flimflam::kApnProperty] = kLastGoodApn;
+  apn_info[flimflam::kApnUsernameProperty] = kLastGoodUsername;
+  // Now when LastGoodAPN is set, it will be the one selected.
+  cellular_->service()->SetLastGoodApn(apn_info);
+  EXPECT_TRUE(props.find(flimflam::kApnProperty) == props.end());
+  capability_->SetupConnectProperties(&props);
+  // We expect the list to contain the last good APN, plus
+  // the user-supplied APN, plus the 4 APNs from the mobile
+  // provider info database.
+  EXPECT_EQ(6, capability_->apn_try_list_.size());
+  EXPECT_FALSE(props.find(flimflam::kApnProperty) == props.end());
+  EXPECT_EQ(kLastGoodApn, props[flimflam::kApnProperty].reader().get_string());
+  EXPECT_FALSE(props.find(flimflam::kApnUsernameProperty) == props.end());
+  EXPECT_EQ(kLastGoodUsername,
+            props[flimflam::kApnUsernameProperty].reader().get_string());
+}
+
 }  // namespace shill
diff --git a/cellular_capability_unittest.cc b/cellular_capability_unittest.cc
index ae5bead..62459c1 100644
--- a/cellular_capability_unittest.cc
+++ b/cellular_capability_unittest.cc
@@ -23,6 +23,7 @@
 #include "shill/mock_modem_gsm_network_proxy.h"
 #include "shill/mock_modem_proxy.h"
 #include "shill/mock_modem_simple_proxy.h"
+#include "shill/mock_profile.h"
 #include "shill/mock_rtnl_handler.h"
 #include "shill/nice_mock_control.h"
 #include "shill/proxy_factory.h"
@@ -65,12 +66,15 @@
         gsm_network_proxy_(new MockModemGSMNetworkProxy()),
         proxy_factory_(this),
         capability_(NULL),
-        device_adaptor_(NULL) {}
+        device_adaptor_(NULL),
+        provider_db_(NULL) {}
 
   virtual ~CellularCapabilityTest() {
     cellular_->service_ = NULL;
     capability_ = NULL;
     device_adaptor_ = NULL;
+    mobile_provider_close_db(provider_db_);
+    provider_db_ = NULL;
   }
 
   virtual void SetUp() {
@@ -84,6 +88,21 @@
     capability_->proxy_factory_ = NULL;
   }
 
+  void InitProviderDB() {
+    provider_db_ = mobile_provider_open_db(kTestMobileProviderDBPath);
+    ASSERT_TRUE(provider_db_);
+    cellular_->provider_db_ = provider_db_;
+  }
+
+  void SetService() {
+    cellular_->service_ = new CellularService(
+        &control_, &dispatcher_, &metrics_, NULL, cellular_);
+  }
+
+  CellularCapabilityGSM *GetGsmCapability() {
+    return dynamic_cast<CellularCapabilityGSM *>(cellular_->capability_.get());
+  }
+
   void InvokeEnable(bool enable, Error *error,
                     const ResultCallback &callback, int timeout) {
     callback.Run(Error());
@@ -116,6 +135,7 @@
   MOCK_METHOD1(TestCallback, void(const Error &error));
 
  protected:
+  static const char kTestMobileProviderDBPath[];
   static const char kTestCarrier[];
   static const char kManufacturer[];
   static const char kModelID[];
@@ -193,8 +213,11 @@
   TestProxyFactory proxy_factory_;
   CellularCapability *capability_;  // Owned by |cellular_|.
   NiceMock<DeviceMockAdaptor> *device_adaptor_;  // Owned by |cellular_|.
+  mobile_provider_db *provider_db_;
 };
 
+const char CellularCapabilityTest::kTestMobileProviderDBPath[] =
+    "provider_db_unittest.bfd";
 const char CellularCapabilityTest::kTestCarrier[] = "The Cellular Carrier";
 const char CellularCapabilityTest::kManufacturer[] = "Company";
 const char CellularCapabilityTest::kModelID[] = "Gobi 2000";
@@ -297,4 +320,76 @@
   EXPECT_EQ(Cellular::kStateRegistered, cellular_->state_);
 }
 
+MATCHER_P(HasApn, apn, "") {
+  DBusPropertiesMap::const_iterator it = arg.find(flimflam::kApnProperty);
+  return it != arg.end() && apn == it->second.reader().get_string();
+}
+
+MATCHER(HasNoApn, "") {
+  return arg.find(flimflam::kApnProperty) == arg.end();
+}
+
+TEST_F(CellularCapabilityTest, TryApns) {
+  static const string kLastGoodApn("remembered.apn");
+  static const string kSuppliedApn("my.apn");
+  static const string kTmobileApn1("epc.tmobile.com");
+  static const string kTmobileApn2("wap.voicestream.com");
+  static const string kTmobileApn3("internet2.voicestream.com");
+  static const string kTmobileApn4("internet3.voicestream.com");
+
+  using testing::InSequence;
+  {
+    InSequence dummy;
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kLastGoodApn), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kSuppliedApn), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kTmobileApn1), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kTmobileApn2), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kTmobileApn3), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasApn(kTmobileApn4), _, _, _));
+    EXPECT_CALL(*simple_proxy_, Connect(HasNoApn(), _, _, _));
+  }
+  CellularCapabilityGSM *gsm_capability = GetGsmCapability();
+  SetService();
+  gsm_capability->imsi_ = "310240123456789";
+  InitProviderDB();
+  gsm_capability->SetHomeProvider();
+  ProfileRefPtr profile(new NiceMock<MockProfile>(
+      &control_, reinterpret_cast<Manager *>(NULL)));
+  cellular_->service()->set_profile(profile);
+
+  Error error;
+  Stringmap apn_info;
+  DBusPropertiesMap props;
+  apn_info[flimflam::kApnProperty] = kSuppliedApn;
+  cellular_->service()->SetApn(apn_info, &error);
+
+  apn_info.clear();
+  apn_info[flimflam::kApnProperty] = kLastGoodApn;
+  cellular_->service()->SetLastGoodApn(apn_info);
+
+  capability_->SetupConnectProperties(&props);
+  // We expect the list to contain the last good APN, plus
+  // the user-supplied APN, plus the 4 APNs from the mobile
+  // provider info database.
+  EXPECT_EQ(6, gsm_capability->apn_try_list_.size());
+  EXPECT_FALSE(props.find(flimflam::kApnProperty) == props.end());
+  EXPECT_EQ(kLastGoodApn, props[flimflam::kApnProperty].reader().get_string());
+
+  SetSimpleProxy();
+  capability_->Connect(props, &error, ResultCallback());
+  Error cerror(Error::kInvalidApn);
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(5, gsm_capability->apn_try_list_.size());
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(4, gsm_capability->apn_try_list_.size());
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(3, gsm_capability->apn_try_list_.size());
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(2, gsm_capability->apn_try_list_.size());
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(1, gsm_capability->apn_try_list_.size());
+  capability_->OnConnectReply(ResultCallback(), cerror);
+  EXPECT_EQ(0, gsm_capability->apn_try_list_.size());
+}
+
 }  // namespace shill
diff --git a/cellular_error.cc b/cellular_error.cc
index 2e3818a..ca404e3 100644
--- a/cellular_error.cc
+++ b/cellular_error.cc
@@ -20,6 +20,8 @@
     MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_SIMPINREQUIRED);
 static const char *kErrorSimPukRequired =
     MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_SIMPUKREQUIRED);
+static const char *kErrorGprsNotSubscribed =
+    MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_GPRSNOTSUBSCRIBED);
 
 // static
 void CellularError::FromDBusError(const DBus::Error &dbus_error,
@@ -42,6 +44,8 @@
     type = Error::kPinRequired;
   else if (name == kErrorSimPukRequired)
     type = Error::kPinBlocked;
+  else if (name == kErrorGprsNotSubscribed)
+    type = Error::kInvalidApn;
   else
     type = Error::kOperationFailed;
 
diff --git a/cellular_service.cc b/cellular_service.cc
index 4c39a3e..74017bc 100644
--- a/cellular_service.cc
+++ b/cellular_service.cc
@@ -12,11 +12,16 @@
 
 #include "shill/adaptor_interfaces.h"
 #include "shill/cellular.h"
+#include "shill/property_accessor.h"
+#include "shill/store_interface.h"
 
 using std::string;
 
 namespace shill {
 
+const char CellularService::kStorageAPN[] = "Cellular.APN";
+const char CellularService::kStorageLastGoodAPN[] = "Cellular.LastGoodAPN";
+
 // TODO(petkov): Add these to system_api/dbus/service_constants.h
 namespace {
 const char kKeyOLPURL[] = "url";
@@ -24,6 +29,17 @@
 const char kKeyOLPPostData[] = "postdata";
 }  // namespace {}
 
+static bool GetNonEmptyField(const Stringmap &stringmap,
+                             const string &fieldname,
+                             string *value) {
+  Stringmap::const_iterator it = stringmap.find(fieldname);
+  if (it != stringmap.end() && !it->second.empty()) {
+    *value = it->second;
+    return true;
+  }
+  return false;
+}
+
 CellularService::OLP::OLP() {
   SetURL("");
   SetMethod("");
@@ -79,7 +95,9 @@
   PropertyStore *store = this->mutable_store();
   store->RegisterConstString(flimflam::kActivationStateProperty,
                              &activation_state_);
-  store->RegisterStringmap(flimflam::kCellularApnProperty, &apn_info_);
+  HelpRegisterDerivedStringmap(flimflam::kCellularApnProperty,
+                               &CellularService::GetApn,
+                               &CellularService::SetApn);
   store->RegisterConstStringmap(flimflam::kCellularLastGoodApnProperty,
                                 &last_good_apn_info_);
   store->RegisterConstString(flimflam::kNetworkTechnologyProperty,
@@ -98,6 +116,144 @@
 
 CellularService::~CellularService() { }
 
+void CellularService::HelpRegisterDerivedStringmap(
+    const string &name,
+    Stringmap(CellularService::*get)(Error *error),
+    void(CellularService::*set)(
+        const Stringmap &value, Error *error)) {
+  mutable_store()->RegisterDerivedStringmap(
+      name,
+      StringmapAccessor(
+          new CustomAccessor<CellularService, Stringmap>(this, get, set)));
+}
+
+Stringmap *CellularService::GetUserSpecifiedApn() {
+  Stringmap::iterator it = apn_info_.find(flimflam::kApnProperty);
+  if (it == apn_info_.end() || it->second.empty())
+    return NULL;
+  return &apn_info_;
+}
+
+Stringmap *CellularService::GetLastGoodApn() {
+  Stringmap::iterator it =
+      last_good_apn_info_.find(flimflam::kApnProperty);
+  if (it == last_good_apn_info_.end() || it->second.empty())
+    return NULL;
+  return &last_good_apn_info_;
+}
+
+Stringmap CellularService::GetApn(Error */*error*/) {
+  return apn_info_;
+}
+
+void CellularService::SetApn(const Stringmap &value, Error *error) {
+  // Only copy in the fields we care about, and validate the contents.
+  // The "apn" field is mandatory.
+  string str;
+  if (!GetNonEmptyField(value, flimflam::kApnProperty, &str)) {
+    error->Populate(Error::kInvalidArguments,
+                    "supplied APN info is missing the apn");
+    return;
+  }
+  apn_info_[flimflam::kApnProperty] = str;
+  if (GetNonEmptyField(value, flimflam::kApnUsernameProperty, &str))
+    apn_info_[flimflam::kApnUsernameProperty] = str;
+  if (GetNonEmptyField(value, flimflam::kApnPasswordProperty, &str))
+    apn_info_[flimflam::kApnPasswordProperty] = str;
+
+  ClearLastGoodApn();
+  adaptor()->EmitStringmapChanged(flimflam::kCellularApnProperty, apn_info_);
+  SaveToCurrentProfile();
+}
+
+void CellularService::SetLastGoodApn(const Stringmap &apn_info) {
+  last_good_apn_info_ = apn_info;
+  adaptor()->EmitStringmapChanged(flimflam::kCellularLastGoodApnProperty,
+                                  last_good_apn_info_);
+  SaveToCurrentProfile();
+}
+
+void CellularService::ClearLastGoodApn() {
+  last_good_apn_info_.clear();
+  adaptor()->EmitStringmapChanged(flimflam::kCellularLastGoodApnProperty,
+                                  last_good_apn_info_);
+  SaveToCurrentProfile();
+}
+
+bool CellularService::Load(StoreInterface *storage) {
+  // Load properties common to all Services.
+  if (!Service::Load(storage))
+    return false;
+
+  const string id = GetStorageIdentifier();
+  LoadApn(storage, id, kStorageAPN, &apn_info_);
+  LoadApn(storage, id, kStorageLastGoodAPN, &last_good_apn_info_);
+  return true;
+}
+
+void CellularService::LoadApn(StoreInterface *storage,
+                              const string &storage_group,
+                              const string &keytag,
+                              Stringmap *apn_info) {
+  if (!LoadApnField(storage, storage_group, keytag,
+               flimflam::kApnProperty, apn_info))
+    return;
+  LoadApnField(storage, storage_group, keytag,
+               flimflam::kApnUsernameProperty, apn_info);
+  LoadApnField(storage, storage_group, keytag,
+               flimflam::kApnPasswordProperty, apn_info);
+}
+
+bool CellularService::LoadApnField(StoreInterface *storage,
+                                   const string &storage_group,
+                                   const string &keytag,
+                                   const string &apntag,
+                                   Stringmap *apn_info) {
+  string value;
+  if (storage->GetString(storage_group, keytag + "." + apntag, &value) &&
+      !value.empty()) {
+    (*apn_info)[apntag] = value;
+    return true;
+  }
+  return false;
+}
+
+bool CellularService::Save(StoreInterface *storage) {
+  // Save properties common to all Services.
+  if (!Service::Save(storage))
+    return false;
+
+  const string id = GetStorageIdentifier();
+  SaveApn(storage, id, GetUserSpecifiedApn(), kStorageAPN);
+  SaveApn(storage, id, GetLastGoodApn(), kStorageLastGoodAPN);
+  return true;
+}
+
+void CellularService::SaveApn(StoreInterface *storage,
+                              const string &storage_group,
+                              const Stringmap *apn_info,
+                              const string &keytag) {
+    SaveApnField(storage, storage_group, apn_info, keytag,
+                 flimflam::kApnProperty);
+    SaveApnField(storage, storage_group, apn_info, keytag,
+                 flimflam::kApnUsernameProperty);
+    SaveApnField(storage, storage_group, apn_info, keytag,
+                 flimflam::kApnPasswordProperty);
+}
+
+void CellularService::SaveApnField(StoreInterface *storage,
+                                   const string &storage_group,
+                                   const Stringmap *apn_info,
+                                   const string &keytag,
+                                   const string &apntag) {
+  const string key = keytag + "." + apntag;
+  string str;
+  if (apn_info && GetNonEmptyField(*apn_info, apntag, &str))
+    storage->SetString(storage_group, key, str);
+  else
+    storage->DeleteKey(storage_group, key);
+}
+
 void CellularService::Connect(Error *error) {
   Service::Connect(error);
   cellular_->Connect(error);
diff --git a/cellular_service.h b/cellular_service.h
index 8e2a3a1..a502f8a 100644
--- a/cellular_service.h
+++ b/cellular_service.h
@@ -24,6 +24,9 @@
 
 class CellularService : public Service {
  public:
+  static const char kStorageAPN[];
+  static const char kStorageLastGoodAPN[];
+
   // Online payment portal.
   class OLP {
    public:
@@ -88,12 +91,50 @@
   void SetRoamingState(const std::string &state);
   const std::string &roaming_state() const { return roaming_state_; }
 
+  // Overrride Load and Save from parent Service class.  We will call
+  // the parent method.
+  virtual bool Load(StoreInterface *storage);
+  virtual bool Save(StoreInterface *storage);
+
+  Stringmap *GetUserSpecifiedApn();
+  Stringmap *GetLastGoodApn();
+  void SetLastGoodApn(const Stringmap &apn_info);
+  void ClearLastGoodApn();
+
  private:
   friend class CellularServiceTest;
+  FRIEND_TEST(CellularCapabilityGSMTest, SetupApnTryList);
+  FRIEND_TEST(CellularCapabilityTest, TryApns);
   FRIEND_TEST(CellularTest, Connect);
 
+  void HelpRegisterDerivedStringmap(
+      const std::string &name,
+      Stringmap(CellularService::*get)(Error *error),
+      void(CellularService::*set)(const Stringmap &value, Error *error));
+
   virtual std::string GetDeviceRpcId(Error *error);
 
+  Stringmap GetApn(Error *error);
+  void SetApn(const Stringmap &value, Error *error);
+  static void SaveApn(StoreInterface *storage,
+                      const std::string &storage_group,
+                      const Stringmap *apn_info,
+                      const std::string &keytag);
+  static void SaveApnField(StoreInterface *storage,
+                           const std::string &storage_group,
+                           const Stringmap *apn_info,
+                           const std::string &keytag,
+                           const std::string &apntag);
+  static void LoadApn(StoreInterface *storage,
+                      const std::string &storage_group,
+                      const std::string &keytag,
+                      Stringmap *apn_info);
+  static bool LoadApnField(StoreInterface *storage,
+                           const std::string &storage_group,
+                           const std::string &keytag,
+                           const std::string &apntag,
+                           Stringmap *apn_info);
+
   // Properties
   std::string activation_state_;
   Cellular::Operator serving_operator_;
@@ -102,8 +143,8 @@
   OLP olp_;
   std::string usage_url_;
 
-  std::map<std::string, std::string> apn_info_;
-  std::map<std::string, std::string> last_good_apn_info_;
+  Stringmap apn_info_;
+  Stringmap last_good_apn_info_;
 
   std::string storage_identifier_;
 
diff --git a/dbus_adaptor.cc b/dbus_adaptor.cc
index 90352cf..3873c6f 100644
--- a/dbus_adaptor.cc
+++ b/dbus_adaptor.cc
@@ -142,6 +142,12 @@
       (*out)[it.Key()]= StringmapToVariant(it.Value(&e));
   }
   {
+    ReadablePropertyConstIterator<Stringmaps> it =
+        store.GetStringmapsPropertiesIter();
+    for ( ; !it.AtEnd(); it.Advance())
+      (*out)[it.Key()]= StringmapsToVariant(it.Value(&e));
+  }
+  {
     ReadablePropertyConstIterator<Strings> it =
         store.GetStringsPropertiesIter();
     for ( ; !it.AtEnd(); it.Advance())
diff --git a/error.cc b/error.cc
index 23aa4ff..476c42b 100644
--- a/error.cc
+++ b/error.cc
@@ -40,6 +40,7 @@
   { "IncorrectPin", "Incorrect PIN" },
   { "PinRequired", "SIM PIN is required"},
   { "PinBlocked", "SIM PIN is blocked"},
+  { "InvalidApn", "Invalid APN" },
   { "PermissionDenied", "Permission denied" }
 };
 
diff --git a/error.h b/error.h
index 001be8a..ca1adee 100644
--- a/error.h
+++ b/error.h
@@ -42,6 +42,7 @@
     kIncorrectPin,
     kPinRequired,
     kPinBlocked,
+    kInvalidApn,
     kPermissionDenied,
     kNumErrors
   };
diff --git a/property_store.cc b/property_store.cc
index 8e8a35c..2959370 100644
--- a/property_store.cc
+++ b/property_store.cc
@@ -428,6 +428,13 @@
   strings_properties_[name] = accessor;
 }
 
+void PropertyStore::RegisterDerivedStringmap(const string &name,
+                                             const StringmapAccessor &acc) {
+  DCHECK(!Contains(name) || ContainsKey(stringmap_properties_, name))
+      << "(Already registered " << name << ")";
+  stringmap_properties_[name] = acc;
+}
+
 void PropertyStore::RegisterDerivedStringmaps(const string &name,
                                               const StringmapsAccessor &acc) {
   DCHECK(!Contains(name) || ContainsKey(stringmaps_properties_, name))
diff --git a/property_store.h b/property_store.h
index f3c0b2e..54c8056 100644
--- a/property_store.h
+++ b/property_store.h
@@ -156,6 +156,8 @@
                                      const RpcIdentifiersAccessor &accessor);
   void RegisterDerivedString(const std::string &name,
                              const StringAccessor &accessor);
+  void RegisterDerivedStringmap(const std::string &name,
+                                const StringmapAccessor &accessor);
   void RegisterDerivedStringmaps(const std::string &name,
                                  const StringmapsAccessor &accessor);
   void RegisterDerivedStrings(const std::string &name,