chromium/chromeos/ash/services/libassistant/device_settings_controller.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/libassistant/device_settings_controller.h"

#include <memory>
#include <utility>

#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "chromeos/ash/services/libassistant/grpc/assistant_client.h"
#include "chromeos/ash/services/libassistant/public/mojom/device_settings_delegate.mojom.h"
#include "chromeos/ash/services/libassistant/util.h"
#include "chromeos/assistant/internal/internal_util.h"
#include "chromeos/assistant/internal/libassistant/shared_headers.h"
#include "chromeos/assistant/internal/proto/shared/proto/conversation.pb.h"
#include "chromeos/assistant/internal/proto/shared/proto/device_args.pb.h"
#include "chromeos/assistant/internal/proto/shared/proto/v2/internal_options.pb.h"

namespace client_op = ::assistant::api::client_op;

namespace ash::libassistant {

using mojom::DeviceSettingsDelegate;
using mojom::GetBrightnessResultPtr;

namespace {
// A macro which ensures we are running on the main thread.
#define ENSURE_MOJOM_THREAD(method, ...)                                    \
  if (!mojom_task_runner_->RunsTasksInCurrentSequence()) {                  \
    mojom_task_runner_->PostTask(                                           \
        FROM_HERE,                                                          \
        base::BindOnce(method, weak_factory_.GetWeakPtr(), ##__VA_ARGS__)); \
    return;                                                                 \
  }
}  // namespace

class Setting {
 public:
  explicit Setting(DeviceSettingsDelegate* delegate) : delegate_(*delegate) {}
  Setting(Setting&) = delete;
  Setting& operator=(Setting&) = delete;
  virtual ~Setting() = default;

  virtual const char* setting_id() const = 0;
  virtual void Modify(const client_op::ModifySettingArgs& request) = 0;

  DeviceSettingsDelegate& delegate() { return *delegate_; }

 private:
  const raw_ref<DeviceSettingsDelegate> delegate_;
};

namespace {

constexpr char kWiFiDeviceSettingId[] = "WIFI";
constexpr char kBluetoothDeviceSettingId[] = "BLUETOOTH";
constexpr char kScreenBrightnessDeviceSettingId[] = "BRIGHTNESS_LEVEL";
constexpr char kDoNotDisturbDeviceSettingId[] = "DO_NOT_DISTURB";
constexpr char kNightLightDeviceSettingId[] = "NIGHT_LIGHT_SWITCH";
constexpr char kSwitchAccessDeviceSettingId[] = "SWITCH_ACCESS";

constexpr float kDefaultSliderStep = 0.1f;

void LogUnsupportedChange(client_op::ModifySettingArgs args) {
  LOG(ERROR) << "Unsupported change operation: " << args.change()
             << " for setting " << args.setting_id();
}

void HandleOnOffChange(client_op::ModifySettingArgs modify_setting_args,
                       std::function<void(bool)> on_off_handler) {
  switch (modify_setting_args.change()) {
    case client_op::ModifySettingArgs_Change_ON:
      on_off_handler(true);
      return;
    case client_op::ModifySettingArgs_Change_OFF:
      on_off_handler(false);
      return;

    // Currently there are no use-cases for toggling.  This could change in the
    // future.
    case client_op::ModifySettingArgs_Change_TOGGLE:
      break;

    case client_op::ModifySettingArgs_Change_SET:
    case client_op::ModifySettingArgs_Change_INCREASE:
    case client_op::ModifySettingArgs_Change_DECREASE:
    case client_op::ModifySettingArgs_Change_UNSPECIFIED:
      // This shouldn't happen.
      break;
  }
  LogUnsupportedChange(modify_setting_args);
}

// Helper function that converts a slider value sent from the server, either
// absolute or a delta, from a given unit (e.g., STEP), to a percentage.
double ConvertSliderValueToLevel(double value,
                                 client_op::ModifySettingArgs_Unit unit,
                                 double default_value) {
  switch (unit) {
    case client_op::ModifySettingArgs_Unit_RANGE:
      // "set brightness to 20%".
      return value;
    case client_op::ModifySettingArgs_Unit_STEP:
      // "set brightness to 20".  Treat the step as a percentage.
      return value / 100.0f;

    // Currently, factor (e.g., 'double the brightness') and decibel units
    // aren't handled by the backend.  This could change in the future.
    case client_op::ModifySettingArgs_Unit_FACTOR:
    case client_op::ModifySettingArgs_Unit_DECIBEL:
      break;

    case client_op::ModifySettingArgs_Unit_NATIVE:
    case client_op::ModifySettingArgs_Unit_UNKNOWN_UNIT:
      // This shouldn't happen.
      break;
  }
  LOG(ERROR) << "Unsupported slider unit: " << unit;
  return default_value;
}

void HandleSliderChange(client_op::ModifySettingArgs request,
                        std::function<void(double)> set_value_handler,
                        std::function<double()> get_value_handler) {
  switch (request.change()) {
    case client_op::ModifySettingArgs_Change_SET: {
      // For unsupported units, set the value to the current value, for
      // visual feedback.
      double new_value = ConvertSliderValueToLevel(
          request.numeric_value(), request.unit(), get_value_handler());
      set_value_handler(new_value);
      return;
    }

    case client_op::ModifySettingArgs_Change_INCREASE:
    case client_op::ModifySettingArgs_Change_DECREASE: {
      double current_value = get_value_handler();
      double step = kDefaultSliderStep;
      if (request.numeric_value() != 0.0f) {
        // For unsupported units, use the default step percentage.
        step = ConvertSliderValueToLevel(request.numeric_value(),
                                         request.unit(), kDefaultSliderStep);
      }
      double new_value =
          (request.change() == client_op::ModifySettingArgs_Change_INCREASE)
              ? std::min(current_value + step, 1.0)
              : std::max(current_value - step, 0.0);
      set_value_handler(new_value);
      return;
    }

    case client_op::ModifySettingArgs_Change_ON:
    case client_op::ModifySettingArgs_Change_OFF:
    case client_op::ModifySettingArgs_Change_TOGGLE:
    case client_op::ModifySettingArgs_Change_UNSPECIFIED:
      // This shouldn't happen.
      break;
  }
  LogUnsupportedChange(request);
}

class WifiSetting : public Setting {
 public:
  using Setting::Setting;

  const char* setting_id() const override { return kWiFiDeviceSettingId; }

  void Modify(const client_op::ModifySettingArgs& request) override {
    HandleOnOffChange(
        request, [&](bool enabled) { delegate().SetWifiEnabled(enabled); });
  }
};

class BluetoothSetting : public Setting {
 public:
  using Setting::Setting;

  const char* setting_id() const override { return kBluetoothDeviceSettingId; }

  void Modify(const client_op::ModifySettingArgs& request) override {
    HandleOnOffChange(request, [&](bool enabled) {
      VLOG(1) << "Assistant: Setting bluetooth enabled: " << enabled;
      delegate().SetBluetoothEnabled(enabled);
    });
  }
};

class DoNotDisturbSetting : public Setting {
 public:
  using Setting::Setting;

  const char* setting_id() const override {
    return kDoNotDisturbDeviceSettingId;
  }

  void Modify(const client_op::ModifySettingArgs& request) override {
    HandleOnOffChange(request, [&](bool enabled) {
      VLOG(1) << "Assistant: Setting do not disturb enabled: " << enabled;
      delegate().SetDoNotDisturbEnabled(enabled);
    });
  }
};

class SwitchAccessSetting : public Setting {
 public:
  using Setting::Setting;

  const char* setting_id() const override {
    return kSwitchAccessDeviceSettingId;
  }

  void Modify(const client_op::ModifySettingArgs& request) override {
    HandleOnOffChange(request, [&](bool enabled) {
      VLOG(1) << "Assistant: Setting switch access enabled: " << enabled;
      delegate().SetSwitchAccessEnabled(enabled);
    });
  }
};

class NightLightSetting : public Setting {
 public:
  using Setting::Setting;

  const char* setting_id() const override { return kNightLightDeviceSettingId; }

  void Modify(const client_op::ModifySettingArgs& request) override {
    HandleOnOffChange(request, [&](bool enabled) {
      VLOG(1) << "Assistant: Setting night light enabled: " << enabled;
      delegate().SetNightLightEnabled(enabled);
    });
  }
};

class BrightnessSetting : public Setting {
 public:
  explicit BrightnessSetting(DeviceSettingsDelegate* delegate)
      : Setting(delegate), weak_factory_(this) {}

  const char* setting_id() const override {
    return kScreenBrightnessDeviceSettingId;
  }

  void Modify(const client_op::ModifySettingArgs& request) override {
    delegate().GetScreenBrightnessLevel(base::BindOnce(
        [](base::WeakPtr<BrightnessSetting> this_,
           client_op::ModifySettingArgs request,
           GetBrightnessResultPtr result) {
          if (!result || !this_) {
            LOG(WARNING) << "Failed to get brightness level";
            return;
          }
          HandleSliderChange(
              request,
              [&this_](double new_value) {
                VLOG(1) << "Assistant: Setting brightness to " << new_value
                        << " percent";
                this_->delegate().SetScreenBrightnessLevel(new_value, true);
              },
              [current_value = result->level]() { return current_value; });
        },
        weak_factory_.GetWeakPtr(), request));
  }

 private:
  base::WeakPtrFactory<BrightnessSetting> weak_factory_;
};

}  // namespace

DeviceSettingsController::DeviceSettingsController()
    : mojom_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
DeviceSettingsController::~DeviceSettingsController() = default;

void DeviceSettingsController::Bind(
    mojo::PendingRemote<mojom::DeviceSettingsDelegate> remote) {
  remote_.Bind(std::move(remote));

  AddSetting(std::make_unique<WifiSetting>(remote_.get()));
  AddSetting(std::make_unique<BluetoothSetting>(remote_.get()));
  AddSetting(std::make_unique<NightLightSetting>(remote_.get()));
  AddSetting(std::make_unique<DoNotDisturbSetting>(remote_.get()));
  AddSetting(std::make_unique<BrightnessSetting>(remote_.get()));
  AddSetting(std::make_unique<SwitchAccessSetting>(remote_.get()));
}

void DeviceSettingsController::OnModifyDeviceSetting(
    const client_op::ModifySettingArgs& modify_setting_args) {
  ENSURE_MOJOM_THREAD(&DeviceSettingsController::OnModifyDeviceSetting,
                      modify_setting_args);
  VLOG(1) << "Assistant: Modifying Device Setting '"
          << modify_setting_args.setting_id() << "'";
  DCHECK(IsSettingSupported(modify_setting_args.setting_id()));

  for (const auto& setting : settings_) {
    if (setting->setting_id() == modify_setting_args.setting_id()) {
      setting->Modify(modify_setting_args);
      return;
    }
  }

  NOTREACHED_IN_MIGRATION();
}

void DeviceSettingsController::OnGetDeviceSettings(
    int interaction_id,
    const ::assistant::api::client_op::GetDeviceSettingsArgs& args) {
  if (!assistant_client_) {
    VLOG(1) << "Assistant: Dropping OnGetDeviceSettings call as Libassistant "
               "has not started yet";
    return;
  }

  std::vector<chromeos::assistant::DeviceSetting> result =
      GetSupportedDeviceSettings(args);

  auto interaction_proto = ash::libassistant::CreateGetDeviceSettingInteraction(
      interaction_id, result);

  ::assistant::api::VoicelessOptions options;
  options.set_is_user_initiated(true);

  assistant_client_->SendVoicelessInteraction(
      interaction_proto, /*description=*/"get_settings_result", options,
      base::DoNothing());
}

void DeviceSettingsController::OnAssistantClientCreated(
    AssistantClient* assistant_client) {
  assistant_client_ = assistant_client;
}

void DeviceSettingsController::OnDestroyingAssistantClient(
    AssistantClient* assistant_client) {
  assistant_client_ = nullptr;
}

std::vector<chromeos::assistant::DeviceSetting>
DeviceSettingsController::GetSupportedDeviceSettings(
    const ::assistant::api::client_op::GetDeviceSettingsArgs& args) const {
  std::vector<chromeos::assistant::DeviceSetting> result;
  for (const std::string& setting_id : args.setting_ids())
    result.emplace_back(setting_id, IsSettingSupported(setting_id));
  return result;
}

bool DeviceSettingsController::IsSettingSupported(
    const std::string& setting_id) const {
  return base::Contains(settings_, setting_id, &Setting::setting_id);
}

void DeviceSettingsController::AddSetting(std::unique_ptr<Setting> setting) {
  settings_.push_back(std::move(setting));
}

}  // namespace ash::libassistant