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_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