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