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