/* * 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 #include #include "common/libs/utils/files.h" #include "common/libs/utils/flag_parser.h" #include "common/libs/utils/json.h" #include "host/libs/config/cuttlefish_config.h" namespace cuttlefish { namespace { const char* kCustomActionInstanceID = "instance_id"; 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"; CustomActionInstanceID GetCustomActionInstanceIDFromJson( const Json::Value& dictionary) { CustomActionInstanceID config; config.instance_id = dictionary[kCustomActionInstanceID].asString(); return config; } CustomShellActionConfig GetCustomShellActionConfigFromJson( const Json::Value& dictionary) { CustomShellActionConfig config; // Shell command with one button. Json::Value button_entry = dictionary[kCustomActionButton]; config.button = {button_entry[kCustomActionButtonCommand].asString(), button_entry[kCustomActionButtonTitle].asString(), button_entry[kCustomActionButtonIconName].asString()}; config.shell_command = dictionary[kCustomActionShellCommand].asString(); return config; } CustomActionServerConfig GetCustomActionServerConfigFromJson( const Json::Value& dictionary) { CustomActionServerConfig config; // Action server with possibly multiple buttons. for (const Json::Value& button_entry : dictionary[kCustomActionButtons]) { config.buttons.push_back( {button_entry[kCustomActionButtonCommand].asString(), button_entry[kCustomActionButtonTitle].asString(), button_entry[kCustomActionButtonIconName].asString()}); } config.server = dictionary[kCustomActionServer].asString(); return config; } CustomDeviceStateActionConfig GetCustomDeviceStateActionConfigFromJson( const Json::Value& dictionary) { CustomDeviceStateActionConfig config; // 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.button = {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); } return config; } Json::Value ToJson(const CustomActionInstanceID& custom_action) { Json::Value json; json[kCustomActionInstanceID] = custom_action.instance_id; return json; } Json::Value ToJson(const CustomShellActionConfig& custom_action) { Json::Value json; // Shell command with one button. json[kCustomActionShellCommand] = custom_action.shell_command; json[kCustomActionButton] = Json::Value(); json[kCustomActionButton][kCustomActionButtonCommand] = custom_action.button.command; json[kCustomActionButton][kCustomActionButtonTitle] = custom_action.button.title; json[kCustomActionButton][kCustomActionButtonIconName] = custom_action.button.icon_name; return json; } Json::Value ToJson(const CustomActionServerConfig& custom_action) { Json::Value json; // 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); } return json; } Json::Value ToJson(const CustomDeviceStateActionConfig& custom_action) { Json::Value json; // 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.button.command; json[kCustomActionButton][kCustomActionButtonTitle] = custom_action.button.title; json[kCustomActionButton][kCustomActionButtonIconName] = custom_action.button.icon_name; return json; } std::string DefaultCustomActionConfig() { auto custom_action_config_dir = DefaultHostArtifactsPath("etc/cvd_custom_action_config"); if (DirectoryExists(custom_action_config_dir)) { auto directory_contents_result = DirectoryContents(custom_action_config_dir); CHECK(directory_contents_result.ok()) << directory_contents_result.error().FormatForEnv(); auto custom_action_configs = std::move(*directory_contents_result); // 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 ""; } int get_instance_order(const std::string& id_str) { int instance_index = 0; const auto& config = CuttlefishConfig::Get(); for (const auto& instance : config->Instances()) { if (instance.id() == id_str) { break; } instance_index++; } return instance_index; } 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_[0]; }); custom_action_config_flag_.Setter( [this](const FlagMatch& match) -> Result { if (!match.value.empty() && (match.value == "unset" || match.value == "\"unset\"")) { custom_action_config_.push_back(DefaultCustomActionConfig()); } else if (!match.value.empty() && !FileExists(match.value)) { return CF_ERRF("custom_action_config file \"{}\" does not exist.", match.value); } else { custom_action_config_.push_back(match.value); } return {}; }); // 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) -> Result { // Load the custom action from the --config preset file. if (match.value == "unset" || match.value == "\"unset\"") { AddEmptyJsonCustomActionConfigs(); return {}; } auto custom_action_array = CF_EXPECT( ParseJson(match.value), "Could not read custom actions config flag"); CF_EXPECT(AddJsonCustomActionConfigs(custom_action_array)); return {}; }); } const std::vector CustomShellActions( const std::string& id_str = std::string()) const override { int instance_index = 0; if (instance_actions_.empty()) { // No Custom Action input, return empty vector return {}; } if (!id_str.empty()) { instance_index = get_instance_order(id_str); } if (instance_index >= instance_actions_.size()) { instance_index = 0; } return instance_actions_[instance_index].custom_shell_actions_; } const std::vector CustomActionServers( const std::string& id_str = std::string()) const override { int instance_index = 0; if (instance_actions_.empty()) { // No Custom Action input, return empty vector return {}; } if (!id_str.empty()) { instance_index = get_instance_order(id_str); } if (instance_index >= instance_actions_.size()) { instance_index = 0; } return instance_actions_[instance_index].custom_action_servers_; } const std::vector CustomDeviceStateActions( const std::string& id_str = std::string()) const override { int instance_index = 0; if (instance_actions_.empty()) { // No Custom Action input, return empty vector return {}; } if (!id_str.empty()) { instance_index = get_instance_order(id_str); } if (instance_index >= instance_actions_.size()) { instance_index = 0; } return instance_actions_[instance_index].custom_device_state_actions_; } // ConfigFragment Json::Value Serialize() const override { Json::Value actions_array(Json::arrayValue); for (const auto& each_instance_actions_ : instance_actions_) { actions_array.append( ToJson(each_instance_actions_.custom_action_instance_id_)); for (const auto& action : each_instance_actions_.custom_shell_actions_) { actions_array.append(ToJson(action)); } for (const auto& action : each_instance_actions_.custom_action_servers_) { actions_array.append(ToJson(action)); } for (const auto& action : each_instance_actions_.custom_device_state_actions_) { 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_)}; } Result Process(std::vector& args) override { CF_EXPECT(ConsumeFlags(Flags(), args)); if (custom_action_config_.empty()) { // no custom action flag input custom_action_config_.push_back(DefaultCustomActionConfig()); } for (const auto& config : custom_action_config_) { if (config != "") { std::string config_contents; CF_EXPECT(android::base::ReadFileToString(config, &config_contents)); auto custom_action_array = CF_EXPECT(ParseJson(config_contents)); CF_EXPECTF(AddJsonCustomActionConfigs(custom_action_array), "Failed to parse config at \"{}\"", config); } else { AddEmptyJsonCustomActionConfigs(); } } return {}; } bool WriteGflagsCompatHelpXml(std::ostream& out) const override { return WriteGflagsCompatXml(Flags(), out); } private: struct InstanceActions { std::vector custom_shell_actions_; std::vector custom_action_servers_; std::vector custom_device_state_actions_; CustomActionInstanceID custom_action_instance_id_; }; std::vector Flags() const { return {custom_action_config_flag_, custom_actions_flag_}; } void AddEmptyJsonCustomActionConfigs() { InstanceActions instance_action; instance_action.custom_action_instance_id_.instance_id = std::to_string(instance_actions_.size()); instance_actions_.push_back(instance_action); } 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; } InstanceActions instance_action; instance_action.custom_action_instance_id_.instance_id = "-1"; for (const auto& custom_action : custom_action_array) { // for multi-instances case, assume instance_id, shell_command, // server and device_states comes together before next instance bool has_instance_id = custom_action.isMember(kCustomActionInstanceID); bool has_shell_command = custom_action.isMember(kCustomActionShellCommand); bool has_server = custom_action.isMember(kCustomActionServer); bool has_device_states = custom_action.isMember(kCustomActionDeviceStates); if (!!has_shell_command + !!has_server + !!has_device_states + !!has_instance_id != 1) { LOG(ERROR) << "Custom action must contain exactly one of " "shell_command, server, device_states or instance_id"; return false; } if (has_shell_command) { auto config = GetCustomShellActionConfigFromJson(custom_action); instance_action.custom_shell_actions_.push_back(config); } else if (has_server) { auto config = GetCustomActionServerConfigFromJson(custom_action); instance_action.custom_action_servers_.push_back(config); } else if (has_device_states) { auto config = GetCustomDeviceStateActionConfigFromJson(custom_action); instance_action.custom_device_state_actions_.push_back(config); } else if (has_instance_id) { auto config = GetCustomActionInstanceIDFromJson(custom_action); if (instance_action.custom_action_instance_id_.instance_id != "-1") { // already has instance id, start a new instance instance_actions_.push_back(instance_action); instance_action = InstanceActions(); } instance_action.custom_action_instance_id_ = config; } else { LOG(ERROR) << "Unknown custom action type."; return false; } } if (instance_action.custom_action_instance_id_.instance_id == "-1") { // default id "-1" which means no instance id assigned yet // at this time, just assign the # of instance as ID instance_action.custom_action_instance_id_.instance_id = std::to_string(instance_actions_.size()); } instance_actions_.push_back(instance_action); return true; } ConfigFlag& config_; Flag custom_action_config_flag_; std::vector custom_action_config_; Flag custom_actions_flag_; std::vector instance_actions_; }; } // namespace fruit::Component, CustomActionConfigProvider> CustomActionsComponent() { return fruit::createComponent() .bind() .addMultibinding() .addMultibinding(); } } // namespace cuttlefish