/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "host/libs/config/custom_actions.h" #include #include #include #include #include #include #include #include "common/libs/utils/files.h" #include "common/libs/utils/flag_parser.h" #include "host/libs/config/cuttlefish_config.h" namespace cuttlefish { namespace { const char* kCustomActionShellCommand = "shell_command"; const char* kCustomActionServer = "server"; const char* kCustomActionDeviceStates = "device_states"; const char* kCustomActionDeviceStateLidSwitchOpen = "lid_switch_open"; const char* kCustomActionDeviceStateHingeAngleValue = "hinge_angle_value"; const char* kCustomActionButton = "button"; const char* kCustomActionButtons = "buttons"; const char* kCustomActionButtonCommand = "command"; const char* kCustomActionButtonTitle = "title"; const char* kCustomActionButtonIconName = "icon_name"; std::optional CustomActionConfigFromJson( const Json::Value& dictionary) { bool has_shell_command = dictionary.isMember(kCustomActionShellCommand); bool has_server = dictionary.isMember(kCustomActionServer); bool has_device_states = dictionary.isMember(kCustomActionDeviceStates); if (!!has_shell_command + !!has_server + !!has_device_states != 1) { LOG(ERROR) << "Custom action must contain exactly one of shell_command, " << "server, or device_states"; return {}; } CustomActionConfig config; if (has_shell_command) { // Shell command with one button. Json::Value button_entry = dictionary[kCustomActionButton]; config.buttons = {{button_entry[kCustomActionButtonCommand].asString(), button_entry[kCustomActionButtonTitle].asString(), button_entry[kCustomActionButtonIconName].asString()}}; config.shell_command = dictionary[kCustomActionShellCommand].asString(); } else if (has_server) { // Action server with possibly multiple buttons. for (const Json::Value& button_entry : dictionary[kCustomActionButtons]) { ControlPanelButton button = { button_entry[kCustomActionButtonCommand].asString(), button_entry[kCustomActionButtonTitle].asString(), button_entry[kCustomActionButtonIconName].asString()}; config.buttons.push_back(button); } config.server = dictionary[kCustomActionServer].asString(); } else if (has_device_states) { // Device state(s) with one button. // Each button press cycles to the next state, then repeats to the first. Json::Value button_entry = dictionary[kCustomActionButton]; config.buttons = {{button_entry[kCustomActionButtonCommand].asString(), button_entry[kCustomActionButtonTitle].asString(), button_entry[kCustomActionButtonIconName].asString()}}; for (const Json::Value& device_state_entry : dictionary[kCustomActionDeviceStates]) { DeviceState state; if (device_state_entry.isMember(kCustomActionDeviceStateLidSwitchOpen)) { state.lid_switch_open = device_state_entry[kCustomActionDeviceStateLidSwitchOpen].asBool(); } if (device_state_entry.isMember( kCustomActionDeviceStateHingeAngleValue)) { state.hinge_angle_value = device_state_entry[kCustomActionDeviceStateHingeAngleValue].asInt(); } config.device_states.push_back(state); } } else { LOG(ERROR) << "Unknown custom action type."; return {}; } return config; } Json::Value ToJson(const CustomActionConfig& custom_action) { Json::Value json; if (custom_action.shell_command) { // Shell command with one button. json[kCustomActionShellCommand] = *custom_action.shell_command; json[kCustomActionButton] = Json::Value(); json[kCustomActionButton][kCustomActionButtonCommand] = custom_action.buttons[0].command; json[kCustomActionButton][kCustomActionButtonTitle] = custom_action.buttons[0].title; json[kCustomActionButton][kCustomActionButtonIconName] = custom_action.buttons[0].icon_name; } else if (custom_action.server) { // Action server with possibly multiple buttons. json[kCustomActionServer] = *custom_action.server; json[kCustomActionButtons] = Json::Value(Json::arrayValue); for (const auto& button : custom_action.buttons) { Json::Value button_entry; button_entry[kCustomActionButtonCommand] = button.command; button_entry[kCustomActionButtonTitle] = button.title; button_entry[kCustomActionButtonIconName] = button.icon_name; json[kCustomActionButtons].append(button_entry); } } else if (!custom_action.device_states.empty()) { // Device state(s) with one button. json[kCustomActionDeviceStates] = Json::Value(Json::arrayValue); for (const auto& device_state : custom_action.device_states) { Json::Value device_state_entry; if (device_state.lid_switch_open) { device_state_entry[kCustomActionDeviceStateLidSwitchOpen] = *device_state.lid_switch_open; } if (device_state.hinge_angle_value) { device_state_entry[kCustomActionDeviceStateHingeAngleValue] = *device_state.hinge_angle_value; } json[kCustomActionDeviceStates].append(device_state_entry); } json[kCustomActionButton] = Json::Value(); json[kCustomActionButton][kCustomActionButtonCommand] = custom_action.buttons[0].command; json[kCustomActionButton][kCustomActionButtonTitle] = custom_action.buttons[0].title; json[kCustomActionButton][kCustomActionButtonIconName] = custom_action.buttons[0].icon_name; } else { LOG(FATAL) << "Unknown custom action type."; } return json; } std::string DefaultCustomActionConfig() { auto custom_action_config_dir = DefaultHostArtifactsPath("etc/cvd_custom_action_config"); if (DirectoryExists(custom_action_config_dir)) { auto custom_action_configs = DirectoryContents(custom_action_config_dir); // Two entries are always . and .. if (custom_action_configs.size() > 3) { LOG(ERROR) << "Expected at most one custom action config in " << custom_action_config_dir << ". Please delete extras."; } else if (custom_action_configs.size() == 3) { for (const auto& config : custom_action_configs) { if (android::base::EndsWithIgnoreCase(config, ".json")) { return custom_action_config_dir + "/" + config; } } } } return ""; } class CustomActionConfigImpl : public CustomActionConfigProvider { public: INJECT(CustomActionConfigImpl(ConfigFlag& config)) : config_(config) { custom_action_config_flag_ = GflagsCompatFlag("custom_action_config"); custom_action_config_flag_.Help( "Path to a custom action config JSON. Defaults to the file provided by " "build variable CVD_CUSTOM_ACTION_CONFIG. If this build variable is " "empty then the custom action config will be empty as well."); custom_action_config_flag_.Getter( [this]() { return custom_action_config_; }); custom_action_config_flag_.Setter([this](const FlagMatch& match) { if (!match.value.empty() && !FileExists(match.value)) { LOG(ERROR) << "custom_action_config file \"" << match.value << "\" " << "does not exist."; return false; } custom_action_config_ = match.value; return true; }); // TODO(schuffelen): Access ConfigFlag directly for these values. custom_actions_flag_ = GflagsCompatFlag("custom_actions"); custom_actions_flag_.Help( "Serialized JSON of an array of custom action objects (in the same " "format as custom action config JSON files). For use within --config " "preset config files; prefer --custom_action_config to specify a " "custom config file on the command line. Actions in this flag are " "combined with actions in --custom_action_config."); custom_actions_flag_.Setter([this](const FlagMatch& match) { // Load the custom action from the --config preset file. Json::CharReaderBuilder builder; std::unique_ptr reader(builder.newCharReader()); std::string errorMessage; Json::Value custom_action_array(Json::arrayValue); if (!reader->parse(&*match.value.begin(), &*match.value.end(), &custom_action_array, &errorMessage)) { LOG(ERROR) << "Could not read custom actions config flag: " << errorMessage; return false; } return AddJsonCustomActionConfigs(custom_action_array); }); } const std::vector& CustomActions() const override { return custom_actions_; } // ConfigFragment Json::Value Serialize() const override { Json::Value actions_array(Json::arrayValue); for (const auto& action : CustomActions()) { actions_array.append(ToJson(action)); } return actions_array; } bool Deserialize(const Json::Value& custom_actions_json) override { return AddJsonCustomActionConfigs(custom_actions_json); } // FlagFeature std::string Name() const override { return "CustomActionConfig"; } std::unordered_set Dependencies() const override { return {static_cast(&config_)}; } bool Process(std::vector& args) override { custom_action_config_ = DefaultCustomActionConfig(); if (!ParseFlags(Flags(), args)) { return false; } if (custom_action_config_ != "") { Json::CharReaderBuilder builder; std::ifstream ifs(custom_action_config_); std::string errorMessage; Json::Value custom_action_array(Json::arrayValue); if (!Json::parseFromStream(builder, ifs, &custom_action_array, &errorMessage)) { LOG(ERROR) << "Could not read custom actions config file " << custom_action_config_ << ": " << errorMessage; return false; } return AddJsonCustomActionConfigs(custom_action_array); } return true; } bool WriteGflagsCompatHelpXml(std::ostream& out) const override { return WriteGflagsCompatXml(Flags(), out); } private: std::vector Flags() const { return {custom_action_config_flag_, custom_actions_flag_}; } bool AddJsonCustomActionConfigs(const Json::Value& custom_action_array) { if (custom_action_array.type() != Json::arrayValue) { LOG(ERROR) << "Expected a JSON array of custom actions"; return false; } for (const auto& custom_action_json : custom_action_array) { auto custom_action = CustomActionConfigFromJson(custom_action_json); if (custom_action) { custom_actions_.push_back(*custom_action); } else { LOG(ERROR) << "Validation failed on a custom action"; return false; } } return true; } ConfigFlag& config_; Flag custom_action_config_flag_; std::string custom_action_config_; Flag custom_actions_flag_; std::vector custom_actions_; }; } // namespace fruit::Component, CustomActionConfigProvider> CustomActionsComponent() { return fruit::createComponent() .bind() .addMultibinding() .addMultibinding(); } } // namespace cuttlefish