blob: 87aa9482420eadc7637f54b3d84c2cc253044f27 [file] [log] [blame]
/*
* 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 <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <json/json.h>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
#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().Trace();
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<void> {
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<void> {
// 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<CustomShellActionConfig> 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<CustomActionServerConfig> 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<CustomDeviceStateActionConfig> 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<FlagFeature*> Dependencies() const override {
return {static_cast<FlagFeature*>(&config_)};
}
Result<void> Process(std::vector<std::string>& args) override {
CF_EXPECT(ParseFlags(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<CustomShellActionConfig> custom_shell_actions_;
std::vector<CustomActionServerConfig> custom_action_servers_;
std::vector<CustomDeviceStateActionConfig> custom_device_state_actions_;
CustomActionInstanceID custom_action_instance_id_;
};
std::vector<Flag> 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<std::string> custom_action_config_;
Flag custom_actions_flag_;
std::vector<InstanceActions> instance_actions_;
};
} // namespace
fruit::Component<fruit::Required<ConfigFlag>, CustomActionConfigProvider>
CustomActionsComponent() {
return fruit::createComponent()
.bind<CustomActionConfigProvider, CustomActionConfigImpl>()
.addMultibinding<ConfigFragment, CustomActionConfigProvider>()
.addMultibinding<FlagFeature, CustomActionConfigProvider>();
}
} // namespace cuttlefish