Merge commit '03cd192eceb46f936ad89e6ae04dac130202e18c' into HEAD

* commit '03cd192eceb46f936ad89e6ae04dac130202e18c':
  Add support for the custom colorXY trait
  Persist kInvalidCredentials state
  Fixed headers parsing in CurlHttpClient
  Replace clouddevices with weave in documentation and tests Removed OAuth scope parameter as optional.

Change-Id: If6f63c188d28af515714633d7058fc34c5f006f0
diff --git a/examples/daemon/light/light.cc b/examples/daemon/light/light.cc
index 484b9e1..d54de93 100644
--- a/examples/daemon/light/light.cc
+++ b/examples/daemon/light/light.cc
@@ -19,12 +19,44 @@
 
     device->AddStateDefinitionsFromJson(R"({
       "onOff": {"state": ["on", "standby"]},
-      "brightness": {"brightness": "integer"}
+      "brightness": {"brightness": "integer"},
+      "colorXY": {
+        "colorSetting": {
+          "properties": {
+            "colorX": {"minimum": 0.0, "maximum": 1.0},
+            "colorY": {"minimum": 0.0, "maximum": 1.0}
+          }
+        },
+        "colorCapRed": {
+          "properties": {
+            "colorX": {"minimum": 0.0, "maximum": 1.0},
+            "colorY": {"minimum": 0.0, "maximum": 1.0}
+          }
+        },
+        "colorCapGreen": {
+          "properties": {
+            "colorX": {"minimum": 0.0, "maximum": 1.0},
+            "colorY": {"minimum": 0.0, "maximum": 1.0}
+          }
+        },
+        "colorCapBlue": {
+          "properties": {
+            "colorX": {"minimum": 0.0, "maximum": 1.0},
+            "colorY": {"minimum": 0.0, "maximum": 1.0}
+          }
+        }
+      }
     })");
 
     device->SetStatePropertiesFromJson(R"({
       "onOff":{"state": "standby"},
-      "brightness":{"brightness": 0}
+      "brightness":{"brightness": 0},
+      "colorXY": {
+        "colorSetting": {"colorX": 0, "colorY": 0},
+        "colorCapRed":  {"colorX": 0.674, "colorY": 0.322},
+        "colorCapGreen":{"colorX": 0.408, "colorY": 0.517},
+        "colorCapBlue": {"colorX": 0.168, "colorY": 0.041}
+      }
     })",
                                        nullptr);
 
@@ -46,6 +78,28 @@
              }
            }
         }
+      },
+      "_colorXY": {
+        "_setConfig": {
+          "minimalRole": "user",
+          "parameters": {
+            "_colorSetting": {
+              "type": "object",
+              "properties": {
+                "_colorX": {
+                  "type": "number",
+                  "minimum": 0,
+                  "maximum": 1
+                },
+                "_colorY": {
+                  "type": "number",
+                  "minimum": 0,
+                  "maximum": 1
+                }
+              }
+            }
+          }
+        }
       }
     })");
     device->AddCommandHandler("onOff.setConfig",
@@ -54,6 +108,9 @@
     device->AddCommandHandler("brightness.setConfig",
                               base::Bind(&LightHandler::OnBrightnessSetConfig,
                                          weak_ptr_factory_.GetWeakPtr()));
+    device->AddCommandHandler("_colorXY._setConfig",
+                              base::Bind(&LightHandler::OnColorXYSetConfig,
+                                         weak_ptr_factory_.GetWeakPtr()));
   }
 
  private:
@@ -105,11 +162,51 @@
     cmd->Abort(error.get(), nullptr);
   }
 
+  void OnColorXYSetConfig(const std::weak_ptr<weave::Command>& command) {
+    auto cmd = command.lock();
+    if (!cmd)
+      return;
+    LOG(INFO) << "received command: " << cmd->GetName();
+    auto params = cmd->GetParameters();
+    base::DictionaryValue* colorXY = nullptr;
+    if (params->GetDictionary("_colorSetting", &colorXY)) {
+      bool updateState = false;
+      double X = 0.0;
+      double Y = 0.0;
+      if (colorXY->GetDouble("_colorX", &X)) {
+        color_X_ = X;
+        updateState = true;
+      }
+
+      if (colorXY->GetDouble("_colorY", &Y)) {
+        color_Y_ = Y;
+        updateState = true;
+      }
+
+      if (updateState)
+        UpdateLightState();
+
+      cmd->Complete({}, nullptr);
+      return;
+    }
+
+    weave::ErrorPtr error;
+    weave::Error::AddTo(&error, FROM_HERE, "example", "invalid_parameter_value",
+                        "Invalid parameters");
+    cmd->Abort(error.get(), nullptr);
+  }
+
   void UpdateLightState() {
     base::DictionaryValue state;
     state.SetString("onOff.state", light_status_ ? "on" : "standby");
     state.SetInteger("brightness.brightness", brightness_state_);
+
+    std::unique_ptr<base::DictionaryValue> colorXY(new base::DictionaryValue());
+    colorXY->SetDouble("colorX", color_X_);
+    colorXY->SetDouble("colorY", color_Y_);
+    state.Set("colorXY.colorSetting", colorXY.get());
     device_->SetStateProperties(state, nullptr);
+    colorXY.release();
   }
 
   weave::Device* device_{nullptr};
@@ -117,6 +214,8 @@
   // Simulate the state of the light.
   bool light_status_;
   int32_t brightness_state_;
+  double color_X_{0.0};
+  double color_Y_{0.0};
   base::WeakPtrFactory<LightHandler> weak_ptr_factory_{this};
 };
 
diff --git a/examples/provider/curl_http_client.cc b/examples/provider/curl_http_client.cc
index 32aa4af..774c07b 100644
--- a/examples/provider/curl_http_client.cc
+++ b/examples/provider/curl_http_client.cc
@@ -34,6 +34,27 @@
   return size * nmemb;
 }
 
+size_t HeaderFunction(void* contents, size_t size, size_t nmemb, void* userp) {
+  std::string header(static_cast<const char*>(contents), size * nmemb);
+  auto pos = header.find(':');
+  if (pos != std::string::npos) {
+    std::pair<std::string, std::string> header_pair;
+
+    static const char kSpaces[] = " \t\r\n";
+    header_pair.first = header.substr(0, pos);
+    pos = header.find_first_not_of(kSpaces, pos + 1);
+    if (pos != std::string::npos) {
+      auto last_non_space = header.find_last_not_of(kSpaces);
+      if (last_non_space >= pos)
+        header_pair.second = header.substr(pos, last_non_space - pos + 1);
+    }
+
+    static_cast<provider::HttpClient::Headers*>(userp)
+        ->emplace_back(std::move(header_pair));
+  }
+  return size * nmemb;
+}
+
 std::pair<std::unique_ptr<CurlHttpClient::Response>, ErrorPtr>
 SendRequestBlocking(CurlHttpClient::Method method,
                     const std::string& url,
@@ -76,9 +97,10 @@
   CHECK_EQ(CURLE_OK,
            curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response->data));
   CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION,
-                                      &WriteFunction));
-  CHECK_EQ(CURLE_OK, curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA,
-                                      &response->content_type));
+                                      &HeaderFunction));
+  provider::HttpClient::Headers response_headers;
+  CHECK_EQ(CURLE_OK,
+           curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &response_headers));
 
   CURLcode res = curl_easy_perform(curl.get());
   if (chunk)
@@ -91,20 +113,16 @@
     return {nullptr, std::move(error)};
   }
 
-  const std::string kContentType = "\r\nContent-Type:";
-  auto pos = response->content_type.find(kContentType);
-  if (pos == std::string::npos) {
+  for (const auto& header : response_headers) {
+    if (header.first == "Content-Type")
+      response->content_type = header.second;
+  }
+
+  if (response->content_type.empty()) {
     Error::AddTo(&error, FROM_HERE, "curl", "no_content_header",
                  "Content-Type header is missing");
     return {nullptr, std::move(error)};
   }
-  pos += kContentType.size();
-  auto pos_end = response->content_type.find("\r\n", pos);
-  if (pos_end == std::string::npos) {
-    pos_end = response->content_type.size();
-  }
-
-  response->content_type = response->content_type.substr(pos, pos_end);
 
   CHECK_EQ(CURLE_OK, curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE,
                                        &response->status));
diff --git a/include/weave/device.h b/include/weave/device.h
index e761e0c..19012b5 100644
--- a/include/weave/device.h
+++ b/include/weave/device.h
@@ -24,7 +24,7 @@
 
 // States of Gcd connection.
 enum class GcdState {
-  kUnconfigured,        // We have no credentials.
+  kUnconfigured,        // Device was not registered.
   kConnecting,          // We have credentials but not yet connected.
   kConnected,           // We're registered and connected to the cloud.
   kInvalidCredentials,  // Our registration has been revoked.
diff --git a/src/device_registration_info.cc b/src/device_registration_info.cc
index d2b10a9..751e530 100644
--- a/src/device_registration_info.cc
+++ b/src/device_registration_info.cc
@@ -201,9 +201,9 @@
       SplitAtFirst(response.GetContentType(), ";", true).first;
 
   if (content_type != http::kJson && content_type != http::kPlain) {
-    Error::AddTo(error, FROM_HERE, errors::json::kDomain,
-                 "non_json_content_type",
-                 "Unexpected response content type: " + content_type);
+    Error::AddTo(
+        error, FROM_HERE, errors::json::kDomain, "non_json_content_type",
+        "Unexpected content type: \'" + response.GetContentType() + "\'");
     return std::unique_ptr<base::DictionaryValue>();
   }
 
@@ -262,6 +262,11 @@
   cloud_backoff_entry_.reset(new BackoffEntry{cloud_backoff_policy_.get()});
   oauth2_backoff_entry_.reset(new BackoffEntry{cloud_backoff_policy_.get()});
 
+  bool revoked =
+      !GetSettings().cloud_id.empty() && !HaveRegistrationCredentials();
+  gcd_state_ =
+      revoked ? GcdState::kInvalidCredentials : GcdState::kUnconfigured;
+
   command_manager_->AddCommandDefChanged(
       base::Bind(&DeviceRegistrationInfo::OnCommandDefsChanged,
                  weak_factory_.GetWeakPtr()));
@@ -511,9 +516,8 @@
 void DeviceRegistrationInfo::GetDeviceInfo(
     const CloudRequestDoneCallback& callback) {
   ErrorPtr error;
-  if (!VerifyRegistrationCredentials(&error)) {
+  if (!VerifyRegistrationCredentials(&error))
     return callback.Run({}, std::move(error));
-  }
   DoCloudRequest(HttpClient::Method::kGet, GetDeviceURL(), nullptr, callback);
 }
 
@@ -675,9 +679,8 @@
   // TODO(antonm): Add support for device removal.
 
   ErrorPtr error;
-  if (!VerifyRegistrationCredentials(&error)) {
+  if (!VerifyRegistrationCredentials(&error))
     return data->callback.Run({}, std::move(error));
-  }
 
   if (cloud_backoff_entry_->ShouldRejectRequest()) {
     VLOG(1) << "Cloud request delayed for "
@@ -762,13 +765,13 @@
 
 void DeviceRegistrationInfo::CheckAccessTokenError(ErrorPtr error) {
   if (error && error->HasError(kErrorDomainOAuth2, "invalid_grant"))
-    MarkDeviceUnregistered();
+    RemoveCredentials();
 }
 
 void DeviceRegistrationInfo::ConnectToCloud(ErrorPtr error) {
   if (error) {
     if (error->HasError(kErrorDomainOAuth2, "invalid_grant"))
-      MarkDeviceUnregistered();
+      RemoveCredentials();
     return;
   }
 
@@ -1270,10 +1273,10 @@
                  << cloud_id << "'";
     return;
   }
-  MarkDeviceUnregistered();
+  RemoveCredentials();
 }
 
-void DeviceRegistrationInfo::MarkDeviceUnregistered() {
+void DeviceRegistrationInfo::RemoveCredentials() {
   if (!HaveRegistrationCredentials())
     return;
 
@@ -1281,7 +1284,7 @@
 
   LOG(INFO) << "Device is unregistered from the cloud. Deleting credentials";
   Config::Transaction change{config_.get()};
-  change.set_cloud_id("");
+  // Keep cloud_id to switch to detect kInvalidCredentials after restart.
   change.set_robot_account("");
   change.set_refresh_token("");
   change.Commit();
diff --git a/src/device_registration_info.h b/src/device_registration_info.h
index 9d9d21f..1399b37 100644
--- a/src/device_registration_info.h
+++ b/src/device_registration_info.h
@@ -110,12 +110,6 @@
   // Starts GCD device if credentials available.
   void Start();
 
-  // Checks whether we have credentials generated during registration.
-  bool HaveRegistrationCredentials() const;
-  // Calls HaveRegistrationCredentials() and logs an error if no credentials
-  // are available.
-  bool VerifyRegistrationCredentials(ErrorPtr* error) const;
-
   // Updates a command (override from CloudCommandUpdateInterface).
   void UpdateCommand(const std::string& command_id,
                      const base::DictionaryValue& command_patch,
@@ -134,6 +128,12 @@
     return weak_factory_.GetWeakPtr();
   }
 
+  // Checks whether we have credentials generated during registration.
+  bool HaveRegistrationCredentials() const;
+  // Calls HaveRegistrationCredentials() and logs an error if no credentials
+  // are available.
+  bool VerifyRegistrationCredentials(ErrorPtr* error) const;
+
   // Cause DeviceRegistrationInfo to attempt to connect to cloud server on
   // its own later.
   void ScheduleCloudConnection(const base::TimeDelta& delay);
@@ -266,7 +266,7 @@
   void OnDeviceDeleted(const std::string& cloud_id) override;
 
   // Wipes out the device registration information and stops server connections.
-  void MarkDeviceUnregistered();
+  void RemoveCredentials();
 
   void RegisterDeviceError(const DoneCallback& callback, ErrorPtr error);
   void RegisterDeviceOnTicketSent(
diff --git a/src/device_registration_info_unittest.cc b/src/device_registration_info_unittest.cc
index 9edd396..df4a438 100644
--- a/src/device_registration_info_unittest.cc
+++ b/src/device_registration_info_unittest.cc
@@ -186,6 +186,10 @@
 
   GcdState GetGcdState() const { return dev_reg_->GetGcdState(); }
 
+  bool HaveRegistrationCredentials() const {
+    return dev_reg_->HaveRegistrationCredentials();
+  }
+
   provider::test::FakeTaskRunner task_runner_;
   provider::test::MockConfigStore config_store_;
   StrictMock<MockHttpClient> http_client_;
@@ -222,14 +226,13 @@
   url += "client_id=";
   url += test_data::kClientId;
   EXPECT_EQ(url, dev_reg_->GetOAuthURL(
-                     "auth",
-                     {{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"},
-                      {"response_type", "code"},
-                      {"client_id", test_data::kClientId}}));
+                     "auth", {{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"},
+                              {"response_type", "code"},
+                              {"client_id", test_data::kClientId}}));
 }
 
 TEST_F(DeviceRegistrationInfoTest, HaveRegistrationCredentials) {
-  EXPECT_FALSE(dev_reg_->HaveRegistrationCredentials());
+  EXPECT_FALSE(HaveRegistrationCredentials());
   ReloadSettings();
 
   EXPECT_CALL(
@@ -254,7 +257,7 @@
       })));
 
   EXPECT_TRUE(RefreshAccessToken(nullptr));
-  EXPECT_TRUE(dev_reg_->HaveRegistrationCredentials());
+  EXPECT_TRUE(HaveRegistrationCredentials());
 }
 
 TEST_F(DeviceRegistrationInfoTest, CheckAuthenticationFailure) {
@@ -313,6 +316,7 @@
   EXPECT_FALSE(RefreshAccessToken(&error));
   EXPECT_TRUE(error->HasError(kErrorDomainOAuth2, "invalid_grant"));
   EXPECT_EQ(GcdState::kInvalidCredentials, GetGcdState());
+  EXPECT_EQ(test_data::kDeviceId, dev_reg_->GetSettings().cloud_id);
 }
 
 TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) {
diff --git a/src/privet/cloud_delegate.cc b/src/privet/cloud_delegate.cc
index b0eea41..d612668 100644
--- a/src/privet/cloud_delegate.cc
+++ b/src/privet/cloud_delegate.cc
@@ -223,7 +223,8 @@
   void OnConfigChanged(const Settings&) { NotifyOnDeviceInfoChanged(); }
 
   void OnRegistrationChanged(GcdState status) {
-    if (status == GcdState::kUnconfigured) {
+    if (status == GcdState::kUnconfigured ||
+        status == GcdState::kInvalidCredentials) {
       connection_state_ = ConnectionState{ConnectionState::kUnconfigured};
     } else if (status == GcdState::kConnecting) {
       // TODO(vitalybuka): Find conditions for kOffline.