buffet: Fix state update notification to GCD cloud server

Device state notification from buffet's state manager are coming
with state property names in form "package_name.property_name" and
that's how they were reported to the GCD server instead of being
split in JSON as {"package_name": {"property_name": value}}.

Also added "buffet_client GetState" command to retrieve actual
internal device state from buffet bypassing the GCD server-dependent
GetDeviceInfo. This is useful for debugging and also in case of
local-only workflow (with no GCD server present).

Finally, modified "buffet_client UpdateState" to allow complex
non-string state properties to be set. Now this command expects
a property name followed by a properly formed json value for the
property. Strings now require double-quotes. Integers, Booleans,
lists and dictionaries can now be set too.

BUG=chromium:449250
TEST=FEATURES=test USE=examples emerge-link buffet
     Deployed and tested on device

Change-Id: Id2c97b422c56ec6648994ab37bb47284a6fb2041
Reviewed-on: https://chromium-review.googlesource.com/241955
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Anton Muhin <antonm@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet_client.cc b/buffet/buffet_client.cc
index 0fa3f58..10efa6e 100644
--- a/buffet/buffet_client.cc
+++ b/buffet/buffet_client.cc
@@ -7,6 +7,7 @@
 #include <sysexits.h>
 
 #include <base/command_line.h>
+#include <base/json/json_reader.h>
 #include <base/logging.h>
 #include <base/memory/ref_counted.h>
 #include <base/strings/stringprintf.h>
@@ -40,10 +41,100 @@
   - RegisterDevice param1=val1&param2=val2...
   - AddCommand '{"name":"command_name","parameters":{}}'
   - UpdateState prop_name prop_value
+  - GetState
   - PendingCommands
 )");
 }
 
+// Helpers for JsonToAny().
+template<typename T>
+chromeos::Any GetJsonValue(const base::Value& json,
+                           bool(base::Value::*fnc)(T*) const) {
+  T val;
+  CHECK((json.*fnc)(&val));
+  return val;
+}
+
+template<typename T>
+chromeos::Any GetJsonList(const base::ListValue& list);  // Prototype.
+
+// Converts a JSON value into an Any so it can be sent over D-Bus using
+// UpdateState D-Bus method from Buffet.
+chromeos::Any JsonToAny(const base::Value& json) {
+  chromeos::Any prop_value;
+  switch (json.GetType()) {
+    case base::Value::TYPE_NULL:
+      prop_value = nullptr;
+      break;
+    case base::Value::TYPE_BOOLEAN:
+      prop_value = GetJsonValue<bool>(json, &base::Value::GetAsBoolean);
+      break;
+    case base::Value::TYPE_INTEGER:
+      prop_value = GetJsonValue<int>(json, &base::Value::GetAsInteger);
+      break;
+    case base::Value::TYPE_DOUBLE:
+      prop_value = GetJsonValue<double>(json, &base::Value::GetAsDouble);
+      break;
+    case base::Value::TYPE_STRING:
+      prop_value = GetJsonValue<std::string>(json, &base::Value::GetAsString);
+      break;
+    case base::Value::TYPE_BINARY:
+      LOG(FATAL) << "Binary values should not happen";
+      break;
+    case base::Value::TYPE_DICTIONARY: {
+      const base::DictionaryValue* dict = nullptr;  // Still owned by |json|.
+      CHECK(json.GetAsDictionary(&dict));
+      chromeos::VariantDictionary var_dict;
+      base::DictionaryValue::Iterator it(*dict);
+      while (!it.IsAtEnd()) {
+        var_dict.emplace(it.key(), JsonToAny(it.value()));
+        it.Advance();
+      }
+      prop_value = var_dict;
+      break;
+    }
+    case base::Value::TYPE_LIST: {
+      const base::ListValue* list = nullptr;  // Still owned by |json|.
+      CHECK(json.GetAsList(&list));
+      CHECK(!list->empty()) << "Unable to deduce the type of list elements.";
+      switch ((*list->begin())->GetType()) {
+        case base::Value::TYPE_BOOLEAN:
+          prop_value = GetJsonList<bool>(*list);
+          break;
+        case base::Value::TYPE_INTEGER:
+          prop_value = GetJsonList<int>(*list);
+          break;
+        case base::Value::TYPE_DOUBLE:
+          prop_value = GetJsonList<double>(*list);
+          break;
+        case base::Value::TYPE_STRING:
+          prop_value = GetJsonList<std::string>(*list);
+          break;
+        case base::Value::TYPE_DICTIONARY:
+          prop_value = GetJsonList<chromeos::VariantDictionary>(*list);
+          break;
+        default:
+          LOG(FATAL) << "Unsupported JSON value type for list element: "
+                     << (*list->begin())->GetType();
+      }
+      break;
+    }
+    default:
+      LOG(FATAL) << "Unexpected JSON value type: " << json.GetType();
+      break;
+  }
+  return prop_value;
+}
+
+template<typename T>
+chromeos::Any GetJsonList(const base::ListValue& list) {
+  std::vector<T> val;
+  val.reserve(list.GetSize());
+  for (const base::Value* v : list)
+    val.push_back(JsonToAny(*v).Get<T>());
+  return val;
+}
+
 class Daemon : public chromeos::DBusDaemon {
  public:
   Daemon() = default;
@@ -94,6 +185,10 @@
                command.compare("us") == 0) {
       if (CheckArgs(command, args, 2))
         PostTask(&Daemon::CallUpdateState, args.front(), args.back());
+    } else if (command.compare("GetState") == 0 ||
+               command.compare("gs") == 0) {
+      if (CheckArgs(command, args, 0))
+        PostTask(&Daemon::CallGetState);
     } else if (command.compare("AddCommand") == 0 ||
                command.compare("ac") == 0) {
       if (CheckArgs(command, args, 1))
@@ -219,13 +314,32 @@
 
   void CallUpdateState(const std::string& prop, const std::string& value) {
     ErrorPtr error;
-    chromeos::VariantDictionary property_set{{prop, value}};
+    std::string error_message;
+    std::unique_ptr<base::Value> json(base::JSONReader::ReadAndReturnError(
+        value, base::JSON_PARSE_RFC, nullptr, &error_message));
+    if (!json) {
+      Error::AddTo(&error, FROM_HERE, chromeos::errors::json::kDomain,
+                   chromeos::errors::json::kParseError, error_message);
+      return ReportError(error.get());
+    }
+
+    chromeos::VariantDictionary property_set{{prop, JsonToAny(*json)}};
     if (!manager_proxy_->UpdateState(property_set, &error)) {
       return ReportError(error.get());
     }
     Quit();
   }
 
+  void CallGetState() {
+    std::string json;
+    ErrorPtr error;
+    if (!manager_proxy_->GetState(&json, &error)) {
+      return ReportError(error.get());
+    }
+    printf("Device State: %s\n", json.c_str());
+    Quit();
+  }
+
   void CallAddCommand(const std::string& command) {
     ErrorPtr error;
     if (!manager_proxy_->AddCommand(command, &error)) {
diff --git a/buffet/dbus_bindings/org.chromium.Buffet.Manager.xml b/buffet/dbus_bindings/org.chromium.Buffet.Manager.xml
index 4490dac..daf1458 100644
--- a/buffet/dbus_bindings/org.chromium.Buffet.Manager.xml
+++ b/buffet/dbus_bindings/org.chromium.Buffet.Manager.xml
@@ -28,6 +28,10 @@
       <arg name="property_set" type="a{sv}" direction="in"/>
       <annotation name="org.chromium.DBus.Method.Kind" value="async"/>
     </method>
+    <method name="GetState">
+      <arg name="device_info" type="s" direction="out"/>
+      <annotation name="org.chromium.DBus.Method.Kind" value="normal"/>
+    </method>
     <method name="AddCommand">
       <arg name="json_command" type="s" direction="in"/>
       <annotation name="org.chromium.DBus.Method.Kind" value="async"/>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index 0b95c90..fb59f3b 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -805,7 +805,11 @@
       if (!value) {
         return;
       }
-      changes->SetWithoutPathExpansion(pair.first, value.release());
+      // The key in |pair.first| is the full property name in format
+      // "package.property_name", so must use DictionaryValue::Set() instead of
+      // DictionaryValue::SetWithoutPathExpansion to recreate the JSON
+      // property tree properly.
+      changes->Set(pair.first, value.release());
     }
     patch->Set("patch", changes.release());
 
diff --git a/buffet/manager.cc b/buffet/manager.cc
index 171eeac..04fcc36 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -155,6 +155,14 @@
     response->Return();
 }
 
+bool Manager::GetState(chromeos::ErrorPtr* error, std::string* state) {
+  auto json = state_manager_->GetStateValuesAsJson(error);
+  if (!json)
+    return false;
+  base::JSONWriter::Write(json.get(), state);
+  return true;
+}
+
 void Manager::AddCommand(DBusMethodResponse<> response,
                          const std::string& json_command) {
   static int next_id = 0;
diff --git a/buffet/manager.h b/buffet/manager.h
index 318ee74..ab5171f 100644
--- a/buffet/manager.h
+++ b/buffet/manager.h
@@ -63,6 +63,8 @@
   // Handles calls to org.chromium.Buffet.Manager.UpdateState().
   void UpdateState(DBusMethodResponse<> response,
                    const chromeos::VariantDictionary& property_set) override;
+  // Handles calls to org.chromium.Buffet.Manager.GetState().
+  bool GetState(chromeos::ErrorPtr* error, std::string* state) override;
   // Handles calls to org.chromium.Buffet.Manager.AddCommand().
   void AddCommand(DBusMethodResponse<> response,
                   const std::string& json_command) override;