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     // Two entries are always . and ..
180     if (custom_action_configs.size() > 3) {
181       LOG(ERROR) << "Expected at most one custom action config in "
182                  << custom_action_config_dir << ". Please delete extras.";
183     } else if (custom_action_configs.size() == 3) {
184       for (const auto& config : custom_action_configs) {
185         if (android::base::EndsWithIgnoreCase(config, ".json")) {
186           return custom_action_config_dir + "/" + config;
187         }
188       }
189     }
190   }
191   return "";
192 }
193 
get_instance_order(const std::string & id_str)194 int get_instance_order(const std::string& id_str) {
195   int instance_index = 0;
196   const auto& config = CuttlefishConfig::Get();
197   for (const auto& instance : config->Instances()) {
198     if (instance.id() == id_str) {
199       break;
200     }
201     instance_index++;
202   }
203   return instance_index;
204 }
205 
206 class CustomActionConfigImpl : public CustomActionConfigProvider {
207  public:
INJECT(CustomActionConfigImpl (ConfigFlag & config))208   INJECT(CustomActionConfigImpl(ConfigFlag& config)) : config_(config) {
209     custom_action_config_flag_ = GflagsCompatFlag("custom_action_config");
210     custom_action_config_flag_.Help(
211         "Path to a custom action config JSON. Defaults to the file provided by "
212         "build variable CVD_CUSTOM_ACTION_CONFIG. If this build variable is "
213         "empty then the custom action config will be empty as well.");
214     custom_action_config_flag_.Getter(
215         [this]() { return custom_action_config_[0]; });
216     custom_action_config_flag_.Setter(
217         [this](const FlagMatch& match) -> Result<void> {
218           if (!match.value.empty() &&
219               (match.value == "unset" || match.value == "\"unset\"")) {
220             custom_action_config_.push_back(DefaultCustomActionConfig());
221           } else if (!match.value.empty() && !FileExists(match.value)) {
222             return CF_ERRF("custom_action_config file \"{}\" does not exist.",
223                            match.value);
224           } else {
225             custom_action_config_.push_back(match.value);
226           }
227           return {};
228         });
229     // TODO(schuffelen): Access ConfigFlag directly for these values.
230     custom_actions_flag_ = GflagsCompatFlag("custom_actions");
231     custom_actions_flag_.Help(
232         "Serialized JSON of an array of custom action objects (in the same "
233         "format as custom action config JSON files). For use within --config "
234         "preset config files; prefer --custom_action_config to specify a "
235         "custom config file on the command line. Actions in this flag are "
236         "combined with actions in --custom_action_config.");
237     custom_actions_flag_.Setter([this](const FlagMatch& match) -> Result<void> {
238       // Load the custom action from the --config preset file.
239       if (match.value == "unset" || match.value == "\"unset\"") {
240         AddEmptyJsonCustomActionConfigs();
241         return {};
242       }
243       auto custom_action_array = CF_EXPECT(
244           ParseJson(match.value), "Could not read custom actions config flag");
245       CF_EXPECT(AddJsonCustomActionConfigs(custom_action_array));
246       return {};
247     });
248   }
249 
CustomShellActions(const std::string & id_str=std::string ()) const250   const std::vector<CustomShellActionConfig> CustomShellActions(
251       const std::string& id_str = std::string()) const override {
252     int instance_index = 0;
253     if (instance_actions_.empty()) {
254       // No Custom Action input, return empty vector
255       return {};
256     }
257 
258     if (!id_str.empty()) {
259       instance_index = get_instance_order(id_str);
260     }
261     if (instance_index >= instance_actions_.size()) {
262       instance_index = 0;
263     }
264     return instance_actions_[instance_index].custom_shell_actions_;
265   }
266 
CustomActionServers(const std::string & id_str=std::string ()) const267   const std::vector<CustomActionServerConfig> CustomActionServers(
268       const std::string& id_str = std::string()) const override {
269     int instance_index = 0;
270     if (instance_actions_.empty()) {
271       // No Custom Action input, return empty vector
272       return {};
273     }
274 
275     if (!id_str.empty()) {
276       instance_index = get_instance_order(id_str);
277     }
278     if (instance_index >= instance_actions_.size()) {
279       instance_index = 0;
280     }
281     return instance_actions_[instance_index].custom_action_servers_;
282   }
283 
CustomDeviceStateActions(const std::string & id_str=std::string ()) const284   const std::vector<CustomDeviceStateActionConfig> CustomDeviceStateActions(
285       const std::string& id_str = std::string()) const override {
286     int instance_index = 0;
287     if (instance_actions_.empty()) {
288       // No Custom Action input, return empty vector
289       return {};
290     }
291 
292     if (!id_str.empty()) {
293       instance_index = get_instance_order(id_str);
294     }
295     if (instance_index >= instance_actions_.size()) {
296       instance_index = 0;
297     }
298     return instance_actions_[instance_index].custom_device_state_actions_;
299   }
300 
301   // ConfigFragment
Serialize() const302   Json::Value Serialize() const override {
303     Json::Value actions_array(Json::arrayValue);
304     for (const auto& each_instance_actions_ : instance_actions_) {
305       actions_array.append(
306           ToJson(each_instance_actions_.custom_action_instance_id_));
307       for (const auto& action : each_instance_actions_.custom_shell_actions_) {
308         actions_array.append(ToJson(action));
309       }
310       for (const auto& action : each_instance_actions_.custom_action_servers_) {
311         actions_array.append(ToJson(action));
312       }
313       for (const auto& action :
314            each_instance_actions_.custom_device_state_actions_) {
315         actions_array.append(ToJson(action));
316       }
317     }
318     return actions_array;
319   }
Deserialize(const Json::Value & custom_actions_json)320   bool Deserialize(const Json::Value& custom_actions_json) override {
321     return AddJsonCustomActionConfigs(custom_actions_json);
322   }
323 
324   // FlagFeature
Name() const325   std::string Name() const override { return "CustomActionConfig"; }
Dependencies() const326   std::unordered_set<FlagFeature*> Dependencies() const override {
327     return {static_cast<FlagFeature*>(&config_)};
328   }
329 
Process(std::vector<std::string> & args)330   Result<void> Process(std::vector<std::string>& args) override {
331     CF_EXPECT(ConsumeFlags(Flags(), args));
332     if (custom_action_config_.empty()) {
333       // no custom action flag input
334       custom_action_config_.push_back(DefaultCustomActionConfig());
335     }
336     for (const auto& config : custom_action_config_) {
337       if (config != "") {
338         std::string config_contents;
339         CF_EXPECT(android::base::ReadFileToString(config, &config_contents));
340         auto custom_action_array = CF_EXPECT(ParseJson(config_contents));
341         CF_EXPECTF(AddJsonCustomActionConfigs(custom_action_array),
342                    "Failed to parse config at \"{}\"", config);
343       } else {
344         AddEmptyJsonCustomActionConfigs();
345       }
346     }
347     return {};
348   }
WriteGflagsCompatHelpXml(std::ostream & out) const349   bool WriteGflagsCompatHelpXml(std::ostream& out) const override {
350     return WriteGflagsCompatXml(Flags(), out);
351   }
352 
353  private:
354   struct InstanceActions {
355     std::vector<CustomShellActionConfig> custom_shell_actions_;
356     std::vector<CustomActionServerConfig> custom_action_servers_;
357     std::vector<CustomDeviceStateActionConfig> custom_device_state_actions_;
358     CustomActionInstanceID custom_action_instance_id_;
359   };
360 
Flags() const361   std::vector<Flag> Flags() const {
362     return {custom_action_config_flag_, custom_actions_flag_};
363   }
364 
AddEmptyJsonCustomActionConfigs()365   void AddEmptyJsonCustomActionConfigs() {
366     InstanceActions instance_action;
367     instance_action.custom_action_instance_id_.instance_id =
368         std::to_string(instance_actions_.size());
369     instance_actions_.push_back(instance_action);
370   }
371 
AddJsonCustomActionConfigs(const Json::Value & custom_action_array)372   bool AddJsonCustomActionConfigs(const Json::Value& custom_action_array) {
373     if (custom_action_array.type() != Json::arrayValue) {
374       LOG(ERROR) << "Expected a JSON array of custom actions";
375       return false;
376     }
377     InstanceActions instance_action;
378     instance_action.custom_action_instance_id_.instance_id = "-1";
379 
380     for (const auto& custom_action : custom_action_array) {
381       // for multi-instances case, assume instance_id, shell_command,
382       // server and device_states comes together before next instance
383       bool has_instance_id = custom_action.isMember(kCustomActionInstanceID);
384       bool has_shell_command =
385           custom_action.isMember(kCustomActionShellCommand);
386       bool has_server = custom_action.isMember(kCustomActionServer);
387       bool has_device_states =
388           custom_action.isMember(kCustomActionDeviceStates);
389       if (!!has_shell_command + !!has_server + !!has_device_states +
390               !!has_instance_id !=
391           1) {
392         LOG(ERROR) << "Custom action must contain exactly one of "
393                       "shell_command, server, device_states or instance_id";
394         return false;
395       }
396 
397       if (has_shell_command) {
398         auto config = GetCustomShellActionConfigFromJson(custom_action);
399         instance_action.custom_shell_actions_.push_back(config);
400       } else if (has_server) {
401         auto config = GetCustomActionServerConfigFromJson(custom_action);
402         instance_action.custom_action_servers_.push_back(config);
403       } else if (has_device_states) {
404         auto config = GetCustomDeviceStateActionConfigFromJson(custom_action);
405         instance_action.custom_device_state_actions_.push_back(config);
406       } else if (has_instance_id) {
407         auto config = GetCustomActionInstanceIDFromJson(custom_action);
408         if (instance_action.custom_action_instance_id_.instance_id != "-1") {
409           // already has instance id, start a new instance
410           instance_actions_.push_back(instance_action);
411           instance_action = InstanceActions();
412         }
413         instance_action.custom_action_instance_id_ = config;
414       } else {
415         LOG(ERROR) << "Unknown custom action type.";
416         return false;
417       }
418     }
419     if (instance_action.custom_action_instance_id_.instance_id == "-1") {
420       // default id "-1" which means no instance id assigned yet
421       // at this time, just assign the # of instance as ID
422       instance_action.custom_action_instance_id_.instance_id =
423           std::to_string(instance_actions_.size());
424     }
425     instance_actions_.push_back(instance_action);
426     return true;
427   }
428 
429     ConfigFlag& config_;
430     Flag custom_action_config_flag_;
431     std::vector<std::string> custom_action_config_;
432     Flag custom_actions_flag_;
433     std::vector<InstanceActions> instance_actions_;
434   };
435 
436 }  // namespace
437 
438 fruit::Component<fruit::Required<ConfigFlag>, CustomActionConfigProvider>
CustomActionsComponent()439 CustomActionsComponent() {
440   return fruit::createComponent()
441       .bind<CustomActionConfigProvider, CustomActionConfigImpl>()
442       .addMultibinding<ConfigFragment, CustomActionConfigProvider>()
443       .addMultibinding<FlagFeature, CustomActionConfigProvider>();
444 }
445 
446 }  // namespace cuttlefish
447