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