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