• 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/strings.h>
20 #include <json/json.h>
21 
22 #include <fstream>
23 #include <optional>
24 #include <string>
25 #include <vector>
26 
27 #include "common/libs/utils/files.h"
28 #include "common/libs/utils/flag_parser.h"
29 #include "host/libs/config/cuttlefish_config.h"
30 
31 namespace cuttlefish {
32 namespace {
33 
34 const char* kCustomActionShellCommand = "shell_command";
35 const char* kCustomActionServer = "server";
36 const char* kCustomActionDeviceStates = "device_states";
37 const char* kCustomActionDeviceStateLidSwitchOpen = "lid_switch_open";
38 const char* kCustomActionDeviceStateHingeAngleValue = "hinge_angle_value";
39 const char* kCustomActionButton = "button";
40 const char* kCustomActionButtons = "buttons";
41 const char* kCustomActionButtonCommand = "command";
42 const char* kCustomActionButtonTitle = "title";
43 const char* kCustomActionButtonIconName = "icon_name";
44 
CustomActionConfigFromJson(const Json::Value & dictionary)45 std::optional<CustomActionConfig> CustomActionConfigFromJson(
46     const Json::Value& dictionary) {
47   bool has_shell_command = dictionary.isMember(kCustomActionShellCommand);
48   bool has_server = dictionary.isMember(kCustomActionServer);
49   bool has_device_states = dictionary.isMember(kCustomActionDeviceStates);
50   if (!!has_shell_command + !!has_server + !!has_device_states != 1) {
51     LOG(ERROR) << "Custom action must contain exactly one of shell_command, "
52                << "server, or device_states";
53     return {};
54   }
55   CustomActionConfig config;
56   if (has_shell_command) {
57     // Shell command with one button.
58     Json::Value button_entry = dictionary[kCustomActionButton];
59     config.buttons = {{button_entry[kCustomActionButtonCommand].asString(),
60                        button_entry[kCustomActionButtonTitle].asString(),
61                        button_entry[kCustomActionButtonIconName].asString()}};
62     config.shell_command = dictionary[kCustomActionShellCommand].asString();
63   } else if (has_server) {
64     // Action server with possibly multiple buttons.
65     for (const Json::Value& button_entry : dictionary[kCustomActionButtons]) {
66       ControlPanelButton button = {
67           button_entry[kCustomActionButtonCommand].asString(),
68           button_entry[kCustomActionButtonTitle].asString(),
69           button_entry[kCustomActionButtonIconName].asString()};
70       config.buttons.push_back(button);
71     }
72     config.server = dictionary[kCustomActionServer].asString();
73   } else if (has_device_states) {
74     // Device state(s) with one button.
75     // Each button press cycles to the next state, then repeats to the first.
76     Json::Value button_entry = dictionary[kCustomActionButton];
77     config.buttons = {{button_entry[kCustomActionButtonCommand].asString(),
78                        button_entry[kCustomActionButtonTitle].asString(),
79                        button_entry[kCustomActionButtonIconName].asString()}};
80     for (const Json::Value& device_state_entry :
81          dictionary[kCustomActionDeviceStates]) {
82       DeviceState state;
83       if (device_state_entry.isMember(kCustomActionDeviceStateLidSwitchOpen)) {
84         state.lid_switch_open =
85             device_state_entry[kCustomActionDeviceStateLidSwitchOpen].asBool();
86       }
87       if (device_state_entry.isMember(
88               kCustomActionDeviceStateHingeAngleValue)) {
89         state.hinge_angle_value =
90             device_state_entry[kCustomActionDeviceStateHingeAngleValue].asInt();
91       }
92       config.device_states.push_back(state);
93     }
94   } else {
95     LOG(ERROR) << "Unknown custom action type.";
96     return {};
97   }
98   return config;
99 }
100 
ToJson(const CustomActionConfig & custom_action)101 Json::Value ToJson(const CustomActionConfig& custom_action) {
102   Json::Value json;
103   if (custom_action.shell_command) {
104     // Shell command with one button.
105     json[kCustomActionShellCommand] = *custom_action.shell_command;
106     json[kCustomActionButton] = Json::Value();
107     json[kCustomActionButton][kCustomActionButtonCommand] =
108         custom_action.buttons[0].command;
109     json[kCustomActionButton][kCustomActionButtonTitle] =
110         custom_action.buttons[0].title;
111     json[kCustomActionButton][kCustomActionButtonIconName] =
112         custom_action.buttons[0].icon_name;
113   } else if (custom_action.server) {
114     // Action server with possibly multiple buttons.
115     json[kCustomActionServer] = *custom_action.server;
116     json[kCustomActionButtons] = Json::Value(Json::arrayValue);
117     for (const auto& button : custom_action.buttons) {
118       Json::Value button_entry;
119       button_entry[kCustomActionButtonCommand] = button.command;
120       button_entry[kCustomActionButtonTitle] = button.title;
121       button_entry[kCustomActionButtonIconName] = button.icon_name;
122       json[kCustomActionButtons].append(button_entry);
123     }
124   } else if (!custom_action.device_states.empty()) {
125     // Device state(s) with one button.
126     json[kCustomActionDeviceStates] = Json::Value(Json::arrayValue);
127     for (const auto& device_state : custom_action.device_states) {
128       Json::Value device_state_entry;
129       if (device_state.lid_switch_open) {
130         device_state_entry[kCustomActionDeviceStateLidSwitchOpen] =
131             *device_state.lid_switch_open;
132       }
133       if (device_state.hinge_angle_value) {
134         device_state_entry[kCustomActionDeviceStateHingeAngleValue] =
135             *device_state.hinge_angle_value;
136       }
137       json[kCustomActionDeviceStates].append(device_state_entry);
138     }
139     json[kCustomActionButton] = Json::Value();
140     json[kCustomActionButton][kCustomActionButtonCommand] =
141         custom_action.buttons[0].command;
142     json[kCustomActionButton][kCustomActionButtonTitle] =
143         custom_action.buttons[0].title;
144     json[kCustomActionButton][kCustomActionButtonIconName] =
145         custom_action.buttons[0].icon_name;
146   } else {
147     LOG(FATAL) << "Unknown custom action type.";
148   }
149   return json;
150 }
151 
DefaultCustomActionConfig()152 std::string DefaultCustomActionConfig() {
153   auto custom_action_config_dir =
154       DefaultHostArtifactsPath("etc/cvd_custom_action_config");
155   if (DirectoryExists(custom_action_config_dir)) {
156     auto custom_action_configs = DirectoryContents(custom_action_config_dir);
157     // Two entries are always . and ..
158     if (custom_action_configs.size() > 3) {
159       LOG(ERROR) << "Expected at most one custom action config in "
160                  << custom_action_config_dir << ". Please delete extras.";
161     } else if (custom_action_configs.size() == 3) {
162       for (const auto& config : custom_action_configs) {
163         if (android::base::EndsWithIgnoreCase(config, ".json")) {
164           return custom_action_config_dir + "/" + config;
165         }
166       }
167     }
168   }
169   return "";
170 }
171 
172 class CustomActionConfigImpl : public CustomActionConfigProvider {
173  public:
INJECT(CustomActionConfigImpl (ConfigFlag & config))174   INJECT(CustomActionConfigImpl(ConfigFlag& config)) : config_(config) {
175     custom_action_config_flag_ = GflagsCompatFlag("custom_action_config");
176     custom_action_config_flag_.Help(
177         "Path to a custom action config JSON. Defaults to the file provided by "
178         "build variable CVD_CUSTOM_ACTION_CONFIG. If this build variable is "
179         "empty then the custom action config will be empty as well.");
180     custom_action_config_flag_.Getter(
181         [this]() { return custom_action_config_; });
182     custom_action_config_flag_.Setter([this](const FlagMatch& match) {
183       if (!match.value.empty() && !FileExists(match.value)) {
184         LOG(ERROR) << "custom_action_config file \"" << match.value << "\" "
185                    << "does not exist.";
186         return false;
187       }
188       custom_action_config_ = match.value;
189       return true;
190     });
191     // TODO(schuffelen): Access ConfigFlag directly for these values.
192     custom_actions_flag_ = GflagsCompatFlag("custom_actions");
193     custom_actions_flag_.Help(
194         "Serialized JSON of an array of custom action objects (in the same "
195         "format as custom action config JSON files). For use within --config "
196         "preset config files; prefer --custom_action_config to specify a "
197         "custom config file on the command line. Actions in this flag are "
198         "combined with actions in --custom_action_config.");
199     custom_actions_flag_.Setter([this](const FlagMatch& match) {
200       // Load the custom action from the --config preset file.
201       Json::CharReaderBuilder builder;
202       std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
203       std::string errorMessage;
204       Json::Value custom_action_array(Json::arrayValue);
205       if (!reader->parse(&*match.value.begin(), &*match.value.end(),
206                          &custom_action_array, &errorMessage)) {
207         LOG(ERROR) << "Could not read custom actions config flag: "
208                    << errorMessage;
209         return false;
210       }
211       return AddJsonCustomActionConfigs(custom_action_array);
212     });
213   }
214 
CustomActions() const215   const std::vector<CustomActionConfig>& CustomActions() const override {
216     return custom_actions_;
217   }
218 
219   // ConfigFragment
Serialize() const220   Json::Value Serialize() const override {
221     Json::Value actions_array(Json::arrayValue);
222     for (const auto& action : CustomActions()) {
223       actions_array.append(ToJson(action));
224     }
225     return actions_array;
226   }
Deserialize(const Json::Value & custom_actions_json)227   bool Deserialize(const Json::Value& custom_actions_json) override {
228     return AddJsonCustomActionConfigs(custom_actions_json);
229   }
230 
231   // FlagFeature
Name() const232   std::string Name() const override { return "CustomActionConfig"; }
Dependencies() const233   std::unordered_set<FlagFeature*> Dependencies() const override {
234     return {static_cast<FlagFeature*>(&config_)};
235   }
236 
Process(std::vector<std::string> & args)237   bool Process(std::vector<std::string>& args) override {
238     custom_action_config_ = DefaultCustomActionConfig();
239     if (!ParseFlags(Flags(), args)) {
240       return false;
241     }
242     if (custom_action_config_ != "") {
243       Json::CharReaderBuilder builder;
244       std::ifstream ifs(custom_action_config_);
245       std::string errorMessage;
246       Json::Value custom_action_array(Json::arrayValue);
247       if (!Json::parseFromStream(builder, ifs, &custom_action_array,
248                                  &errorMessage)) {
249         LOG(ERROR) << "Could not read custom actions config file "
250                    << custom_action_config_ << ": " << errorMessage;
251         return false;
252       }
253       return AddJsonCustomActionConfigs(custom_action_array);
254     }
255     return true;
256   }
WriteGflagsCompatHelpXml(std::ostream & out) const257   bool WriteGflagsCompatHelpXml(std::ostream& out) const override {
258     return WriteGflagsCompatXml(Flags(), out);
259   }
260 
261  private:
Flags() const262   std::vector<Flag> Flags() const {
263     return {custom_action_config_flag_, custom_actions_flag_};
264   }
265 
AddJsonCustomActionConfigs(const Json::Value & custom_action_array)266   bool AddJsonCustomActionConfigs(const Json::Value& custom_action_array) {
267     if (custom_action_array.type() != Json::arrayValue) {
268       LOG(ERROR) << "Expected a JSON array of custom actions";
269       return false;
270     }
271     for (const auto& custom_action_json : custom_action_array) {
272       auto custom_action = CustomActionConfigFromJson(custom_action_json);
273       if (custom_action) {
274         custom_actions_.push_back(*custom_action);
275       } else {
276         LOG(ERROR) << "Validation failed on a custom action";
277         return false;
278       }
279     }
280     return true;
281   }
282 
283   ConfigFlag& config_;
284   Flag custom_action_config_flag_;
285   std::string custom_action_config_;
286   Flag custom_actions_flag_;
287   std::vector<CustomActionConfig> custom_actions_;
288 };
289 
290 }  // namespace
291 
292 fruit::Component<fruit::Required<ConfigFlag>, CustomActionConfigProvider>
CustomActionsComponent()293 CustomActionsComponent() {
294   return fruit::createComponent()
295       .bind<CustomActionConfigProvider, CustomActionConfigImpl>()
296       .addMultibinding<ConfigFragment, CustomActionConfigProvider>()
297       .addMultibinding<FlagFeature, CustomActionConfigProvider>();
298 }
299 
300 }  // namespace cuttlefish
301