• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 #include "host/libs/config/custom_actions.h"
17 
18 #include <android-base/logging.h>
19 #include <android-base/parseint.h>
20 #include <android-base/strings.h>
21 #include <json/json.h>
22 
23 #include <fstream>
24 #include <optional>
25 #include <string>
26 #include <vector>
27 
28 #include "common/libs/utils/files.h"
29 #include "common/libs/utils/flag_parser.h"
30 #include "host/libs/config/cuttlefish_config.h"
31 
32 namespace cuttlefish {
33 namespace {
34 
35 const char* kCustomActionInstanceID = "instance_id";
36 const char* kCustomActionShellCommand = "shell_command";
37 const char* kCustomActionServer = "server";
38 const char* kCustomActionDeviceStates = "device_states";
39 const char* kCustomActionDeviceStateLidSwitchOpen = "lid_switch_open";
40 const char* kCustomActionDeviceStateHingeAngleValue = "hinge_angle_value";
41 const char* kCustomActionButton = "button";
42 const char* kCustomActionButtons = "buttons";
43 const char* kCustomActionButtonCommand = "command";
44 const char* kCustomActionButtonTitle = "title";
45 const char* kCustomActionButtonIconName = "icon_name";
46 
GetCustomActionInstanceIDFromJson(const Json::Value & dictionary)47 CustomActionInstanceID GetCustomActionInstanceIDFromJson(
48     const Json::Value& dictionary) {
49   CustomActionInstanceID config;
50   config.instance_id = dictionary[kCustomActionInstanceID].asString();
51   return config;
52 }
53 
GetCustomShellActionConfigFromJson(const Json::Value & dictionary)54 CustomShellActionConfig GetCustomShellActionConfigFromJson(
55     const Json::Value& dictionary) {
56   CustomShellActionConfig config;
57   // Shell command with one button.
58   Json::Value button_entry = dictionary[kCustomActionButton];
59   config.button = {button_entry[kCustomActionButtonCommand].asString(),
60     button_entry[kCustomActionButtonTitle].asString(),
61     button_entry[kCustomActionButtonIconName].asString()};
62   config.shell_command = dictionary[kCustomActionShellCommand].asString();
63   return config;
64 }
65 
GetCustomActionServerConfigFromJson(const Json::Value & dictionary)66 CustomActionServerConfig GetCustomActionServerConfigFromJson(
67     const Json::Value& dictionary) {
68   CustomActionServerConfig config;
69   // Action server with possibly multiple buttons.
70   for (const Json::Value& button_entry : dictionary[kCustomActionButtons]) {
71     config.buttons.push_back(
72         {button_entry[kCustomActionButtonCommand].asString(),
73         button_entry[kCustomActionButtonTitle].asString(),
74         button_entry[kCustomActionButtonIconName].asString()});
75   }
76   config.server = dictionary[kCustomActionServer].asString();
77   return config;
78 }
79 
GetCustomDeviceStateActionConfigFromJson(const Json::Value & dictionary)80 CustomDeviceStateActionConfig GetCustomDeviceStateActionConfigFromJson(
81     const Json::Value& dictionary) {
82   CustomDeviceStateActionConfig config;
83   // Device state(s) with one button.
84   // Each button press cycles to the next state, then repeats to the first.
85   Json::Value button_entry = dictionary[kCustomActionButton];
86   config.button = {button_entry[kCustomActionButtonCommand].asString(),
87     button_entry[kCustomActionButtonTitle].asString(),
88     button_entry[kCustomActionButtonIconName].asString()};
89   for (const Json::Value& device_state_entry :
90       dictionary[kCustomActionDeviceStates]) {
91     DeviceState state;
92     if (device_state_entry.isMember(
93           kCustomActionDeviceStateLidSwitchOpen)) {
94       state.lid_switch_open =
95         device_state_entry[kCustomActionDeviceStateLidSwitchOpen].asBool();
96     }
97     if (device_state_entry.isMember(
98           kCustomActionDeviceStateHingeAngleValue)) {
99       state.hinge_angle_value =
100         device_state_entry[kCustomActionDeviceStateHingeAngleValue].asInt();
101     }
102     config.device_states.push_back(state);
103   }
104   return config;
105 }
106 
ToJson(const CustomActionInstanceID & custom_action)107 Json::Value ToJson(const CustomActionInstanceID& custom_action) {
108   Json::Value json;
109   json[kCustomActionInstanceID] = custom_action.instance_id;
110   return json;
111 }
112 
ToJson(const CustomShellActionConfig & custom_action)113 Json::Value ToJson(const CustomShellActionConfig& custom_action) {
114   Json::Value json;
115   // Shell command with one button.
116   json[kCustomActionShellCommand] = custom_action.shell_command;
117   json[kCustomActionButton] = Json::Value();
118   json[kCustomActionButton][kCustomActionButtonCommand] =
119       custom_action.button.command;
120   json[kCustomActionButton][kCustomActionButtonTitle] =
121       custom_action.button.title;
122   json[kCustomActionButton][kCustomActionButtonIconName] =
123       custom_action.button.icon_name;
124   return json;
125 }
126 
ToJson(const CustomActionServerConfig & custom_action)127 Json::Value ToJson(const CustomActionServerConfig& custom_action) {
128   Json::Value json;
129   // Action server with possibly multiple buttons.
130   json[kCustomActionServer] = custom_action.server;
131   json[kCustomActionButtons] = Json::Value(Json::arrayValue);
132   for (const auto& button : custom_action.buttons) {
133     Json::Value button_entry;
134     button_entry[kCustomActionButtonCommand] = button.command;
135     button_entry[kCustomActionButtonTitle] = button.title;
136     button_entry[kCustomActionButtonIconName] = button.icon_name;
137     json[kCustomActionButtons].append(button_entry);
138   }
139   return json;
140 }
141 
ToJson(const CustomDeviceStateActionConfig & custom_action)142 Json::Value ToJson(const CustomDeviceStateActionConfig& custom_action) {
143   Json::Value json;
144   // Device state(s) with one button.
145   json[kCustomActionDeviceStates] = Json::Value(Json::arrayValue);
146   for (const auto& device_state : custom_action.device_states) {
147     Json::Value device_state_entry;
148     if (device_state.lid_switch_open) {
149       device_state_entry[kCustomActionDeviceStateLidSwitchOpen] =
150           *device_state.lid_switch_open;
151     }
152     if (device_state.hinge_angle_value) {
153       device_state_entry[kCustomActionDeviceStateHingeAngleValue] =
154           *device_state.hinge_angle_value;
155     }
156     json[kCustomActionDeviceStates].append(device_state_entry);
157   }
158   json[kCustomActionButton] = Json::Value();
159   json[kCustomActionButton][kCustomActionButtonCommand] =
160       custom_action.button.command;
161   json[kCustomActionButton][kCustomActionButtonTitle] =
162       custom_action.button.title;
163   json[kCustomActionButton][kCustomActionButtonIconName] =
164       custom_action.button.icon_name;
165   return json;
166 }
167 
DefaultCustomActionConfig()168 std::string DefaultCustomActionConfig() {
169   auto custom_action_config_dir =
170       DefaultHostArtifactsPath("etc/cvd_custom_action_config");
171   if (DirectoryExists(custom_action_config_dir)) {
172     auto directory_contents_result =
173         DirectoryContents(custom_action_config_dir);
174     CHECK(directory_contents_result.ok())
175         << directory_contents_result.error().Trace();
176     auto custom_action_configs = std::move(*directory_contents_result);
177     // Two entries are always . and ..
178     if (custom_action_configs.size() > 3) {
179       LOG(ERROR) << "Expected at most one custom action config in "
180                  << custom_action_config_dir << ". Please delete extras.";
181     } else if (custom_action_configs.size() == 3) {
182       for (const auto& config : custom_action_configs) {
183         if (android::base::EndsWithIgnoreCase(config, ".json")) {
184           return custom_action_config_dir + "/" + config;
185         }
186       }
187     }
188   }
189   return "";
190 }
191 
get_instance_order(const std::string & id_str)192 int get_instance_order(const std::string& id_str) {
193   int instance_index = 0;
194   const auto& config = CuttlefishConfig::Get();
195   for (const auto& instance : config->Instances()) {
196     if (instance.id() == id_str) {
197       break;
198     }
199     instance_index++;
200   }
201   return instance_index;
202 }
203 
204 class CustomActionConfigImpl : public CustomActionConfigProvider {
205  public:
INJECT(CustomActionConfigImpl (ConfigFlag & config))206   INJECT(CustomActionConfigImpl(ConfigFlag& config)) : config_(config) {
207     custom_action_config_flag_ = GflagsCompatFlag("custom_action_config");
208     custom_action_config_flag_.Help(
209         "Path to a custom action config JSON. Defaults to the file provided by "
210         "build variable CVD_CUSTOM_ACTION_CONFIG. If this build variable is "
211         "empty then the custom action config will be empty as well.");
212     custom_action_config_flag_.Getter(
213         [this]() { return custom_action_config_[0]; });
214     custom_action_config_flag_.Setter([this](const FlagMatch& match) {
215       if (!match.value.empty() &&
216           (match.value == "unset" || match.value == "\"unset\"")) {
217         custom_action_config_.push_back(DefaultCustomActionConfig());
218       } else if (!match.value.empty() && !FileExists(match.value)) {
219         LOG(ERROR) << "custom_action_config file \"" << match.value << "\" "
220                    << "does not exist.";
221         return false;
222       } else {
223         custom_action_config_.push_back(match.value);
224       }
225       return true;
226     });
227     // TODO(schuffelen): Access ConfigFlag directly for these values.
228     custom_actions_flag_ = GflagsCompatFlag("custom_actions");
229     custom_actions_flag_.Help(
230         "Serialized JSON of an array of custom action objects (in the same "
231         "format as custom action config JSON files). For use within --config "
232         "preset config files; prefer --custom_action_config to specify a "
233         "custom config file on the command line. Actions in this flag are "
234         "combined with actions in --custom_action_config.");
235     custom_actions_flag_.Setter([this](const FlagMatch& match) {
236       // Load the custom action from the --config preset file.
237       if (match.value == "unset" || match.value == "\"unset\"") {
238         AddEmptyJsonCustomActionConfigs();
239         return true;
240       }
241       Json::CharReaderBuilder builder;
242       std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
243       std::string errorMessage;
244       Json::Value custom_action_array(Json::arrayValue);
245       if (!reader->parse(&*match.value.begin(), &*match.value.end(),
246                          &custom_action_array, &errorMessage)) {
247         LOG(ERROR) << "Could not read custom actions config flag: "
248                    << errorMessage;
249         return false;
250       }
251       return AddJsonCustomActionConfigs(custom_action_array);
252     });
253   }
254 
CustomShellActions(const std::string & id_str=std::string ()) const255   const std::vector<CustomShellActionConfig> CustomShellActions(
256       const std::string& id_str = std::string()) const override {
257     int instance_index = 0;
258     if (instance_actions_.empty()) {
259       // No Custom Action input, return empty vector
260       return {};
261     }
262 
263     if (!id_str.empty()) {
264       instance_index = get_instance_order(id_str);
265     }
266     if (instance_index >= instance_actions_.size()) {
267       instance_index = 0;
268     }
269     return instance_actions_[instance_index].custom_shell_actions_;
270   }
271 
CustomActionServers(const std::string & id_str=std::string ()) const272   const std::vector<CustomActionServerConfig> CustomActionServers(
273       const std::string& id_str = std::string()) const override {
274     int instance_index = 0;
275     if (instance_actions_.empty()) {
276       // No Custom Action input, return empty vector
277       return {};
278     }
279 
280     if (!id_str.empty()) {
281       instance_index = get_instance_order(id_str);
282     }
283     if (instance_index >= instance_actions_.size()) {
284       instance_index = 0;
285     }
286     return instance_actions_[instance_index].custom_action_servers_;
287   }
288 
CustomDeviceStateActions(const std::string & id_str=std::string ()) const289   const std::vector<CustomDeviceStateActionConfig> CustomDeviceStateActions(
290       const std::string& id_str = std::string()) const override {
291     int instance_index = 0;
292     if (instance_actions_.empty()) {
293       // No Custom Action input, return empty vector
294       return {};
295     }
296 
297     if (!id_str.empty()) {
298       instance_index = get_instance_order(id_str);
299     }
300     if (instance_index >= instance_actions_.size()) {
301       instance_index = 0;
302     }
303     return instance_actions_[instance_index].custom_device_state_actions_;
304   }
305 
306   // ConfigFragment
Serialize() const307   Json::Value Serialize() const override {
308     Json::Value actions_array(Json::arrayValue);
309     for (const auto& each_instance_actions_ : instance_actions_) {
310       actions_array.append(
311           ToJson(each_instance_actions_.custom_action_instance_id_));
312       for (const auto& action : each_instance_actions_.custom_shell_actions_) {
313         actions_array.append(ToJson(action));
314       }
315       for (const auto& action : each_instance_actions_.custom_action_servers_) {
316         actions_array.append(ToJson(action));
317       }
318       for (const auto& action :
319            each_instance_actions_.custom_device_state_actions_) {
320         actions_array.append(ToJson(action));
321       }
322     }
323     return actions_array;
324   }
Deserialize(const Json::Value & custom_actions_json)325   bool Deserialize(const Json::Value& custom_actions_json) override {
326     return AddJsonCustomActionConfigs(custom_actions_json);
327   }
328 
329   // FlagFeature
Name() const330   std::string Name() const override { return "CustomActionConfig"; }
Dependencies() const331   std::unordered_set<FlagFeature*> Dependencies() const override {
332     return {static_cast<FlagFeature*>(&config_)};
333   }
334 
Process(std::vector<std::string> & args)335   bool Process(std::vector<std::string>& args) override {
336     if (!ParseFlags(Flags(), args)) {
337       return false;
338     }
339     if (custom_action_config_.empty()) {
340       // no custom action flag input
341       custom_action_config_.push_back(DefaultCustomActionConfig());
342     }
343     for (const auto& config : custom_action_config_) {
344       if (config != "") {
345         Json::CharReaderBuilder builder;
346         std::ifstream ifs(config);
347         std::string errorMessage;
348         Json::Value custom_action_array(Json::arrayValue);
349         if (!Json::parseFromStream(builder, ifs, &custom_action_array,
350                                    &errorMessage)) {
351           LOG(ERROR) << "Could not read custom actions config file " << config
352                      << ": " << errorMessage;
353           return false;
354         }
355         if (!AddJsonCustomActionConfigs(custom_action_array)) {
356           return false;
357         }
358       } else {
359         AddEmptyJsonCustomActionConfigs();
360       }
361     }
362     return true;
363   }
WriteGflagsCompatHelpXml(std::ostream & out) const364   bool WriteGflagsCompatHelpXml(std::ostream& out) const override {
365     return WriteGflagsCompatXml(Flags(), out);
366   }
367 
368  private:
369   struct InstanceActions {
370     std::vector<CustomShellActionConfig> custom_shell_actions_;
371     std::vector<CustomActionServerConfig> custom_action_servers_;
372     std::vector<CustomDeviceStateActionConfig> custom_device_state_actions_;
373     CustomActionInstanceID custom_action_instance_id_;
374   };
375 
Flags() const376   std::vector<Flag> Flags() const {
377     return {custom_action_config_flag_, custom_actions_flag_};
378   }
379 
AddEmptyJsonCustomActionConfigs()380   void AddEmptyJsonCustomActionConfigs() {
381     InstanceActions instance_action;
382     instance_action.custom_action_instance_id_.instance_id =
383         std::to_string(instance_actions_.size());
384     instance_actions_.push_back(instance_action);
385   }
386 
AddJsonCustomActionConfigs(const Json::Value & custom_action_array)387   bool AddJsonCustomActionConfigs(const Json::Value& custom_action_array) {
388     if (custom_action_array.type() != Json::arrayValue) {
389       LOG(ERROR) << "Expected a JSON array of custom actions";
390       return false;
391     }
392     InstanceActions instance_action;
393     instance_action.custom_action_instance_id_.instance_id = "-1";
394 
395     for (const auto& custom_action : custom_action_array) {
396       // for multi-instances case, assume instance_id, shell_command,
397       // server and device_states comes together before next instance
398       bool has_instance_id = custom_action.isMember(kCustomActionInstanceID);
399       bool has_shell_command =
400           custom_action.isMember(kCustomActionShellCommand);
401       bool has_server = custom_action.isMember(kCustomActionServer);
402       bool has_device_states =
403           custom_action.isMember(kCustomActionDeviceStates);
404       if (!!has_shell_command + !!has_server + !!has_device_states +
405               !!has_instance_id !=
406           1) {
407         LOG(ERROR) << "Custom action must contain exactly one of "
408                       "shell_command, server, device_states or instance_id";
409         return false;
410       }
411 
412       if (has_shell_command) {
413         auto config = GetCustomShellActionConfigFromJson(custom_action);
414         instance_action.custom_shell_actions_.push_back(config);
415       } else if (has_server) {
416         auto config = GetCustomActionServerConfigFromJson(custom_action);
417         instance_action.custom_action_servers_.push_back(config);
418       } else if (has_device_states) {
419         auto config = GetCustomDeviceStateActionConfigFromJson(custom_action);
420         instance_action.custom_device_state_actions_.push_back(config);
421       } else if (has_instance_id) {
422         auto config = GetCustomActionInstanceIDFromJson(custom_action);
423         if (instance_action.custom_action_instance_id_.instance_id != "-1") {
424           // already has instance id, start a new instance
425           instance_actions_.push_back(instance_action);
426           instance_action = InstanceActions();
427         }
428         instance_action.custom_action_instance_id_ = config;
429       } else {
430         LOG(ERROR) << "Unknown custom action type.";
431         return false;
432       }
433     }
434     if (instance_action.custom_action_instance_id_.instance_id == "-1") {
435       // default id "-1" which means no instance id assigned yet
436       // at this time, just assign the # of instance as ID
437       instance_action.custom_action_instance_id_.instance_id =
438           std::to_string(instance_actions_.size());
439     }
440     instance_actions_.push_back(instance_action);
441     return true;
442   }
443 
444     ConfigFlag& config_;
445     Flag custom_action_config_flag_;
446     std::vector<std::string> custom_action_config_;
447     Flag custom_actions_flag_;
448     std::vector<InstanceActions> instance_actions_;
449   };
450 
451 }  // namespace
452 
453 fruit::Component<fruit::Required<ConfigFlag>, CustomActionConfigProvider>
CustomActionsComponent()454 CustomActionsComponent() {
455   return fruit::createComponent()
456       .bind<CustomActionConfigProvider, CustomActionConfigImpl>()
457       .addMultibinding<ConfigFragment, CustomActionConfigProvider>()
458       .addMultibinding<FlagFeature, CustomActionConfigProvider>();
459 }
460 
461 }  // namespace cuttlefish
462