text: // Copyright 2020 The Fuchsia Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include <lib/syslog/cpp/macros.h>
  
  #include <optional>
  
  #include "fuchsia/accessibility/semantics/cpp/fidl.h"
  // This header file has been generated from the strings library fuchsia.intl.l10n.
  #include "fuchsia/intl/l10n/cpp/fidl.h"
  #include "src/ui/a11y/lib/screen_reader/screen_reader_message_generator.h"
  #include "src/ui/a11y/lib/screen_reader/util/util.h"
  
  namespace a11y {
  namespace {
  
  using fuchsia::accessibility::semantics::Node;
  using fuchsia::accessibility::semantics::Role;
  using fuchsia::accessibility::tts::Utterance;
  using fuchsia::intl::l10n::MessageIds;
  
  static constexpr zx::duration kDefaultDelay = zx::msec(40);
  static constexpr zx::duration kLongDelay = zx::msec(100);
  
  // Returns a message that describes the label and range value of a slider.
  std::string GetSliderLabelAndRangeMessage(const fuchsia::accessibility::semantics::Node* node) {
    std::string message;
    if (node->has_attributes() && node->attributes().has_label()) {
      message += node->attributes().label();
    }
  
    if (node->has_states() && node->states().has_range_value()) {
      message = message + ", " + FormatFloat(node->states().range_value());
    }
  
    return message;
  }
  
  // Returns true if the node is clickable in any way (normal click, long click).
  bool NodeIsClickable(const Node* node) {
    if (!node->has_actions()) {
      return false;
    }
    for (const auto& action : node->actions()) {
      if (action == fuchsia::accessibility::semantics::Action::DEFAULT) {
        return true;
      }
    }
    return false;
  }
  
  }  // namespace
  
  ScreenReaderMessageGenerator::ScreenReaderMessageGenerator(
      std::unique_ptr<i18n::MessageFormatter> message_formatter)
      : message_formatter_(std::move(message_formatter)) {}
  
  std::vector<ScreenReaderMessageGenerator::UtteranceAndContext>
  ScreenReaderMessageGenerator::DescribeNode(const Node* node) {
    std::vector<UtteranceAndContext> description;
    {
      // If this node is a radio button, slider, or toggle switch, the label is part of the whole
      // message that describes it.
      if (node->has_role() && node->role() == fuchsia::accessibility::semantics::Role::RADIO_BUTTON) {
        description.emplace_back(DescribeRadioButton(node));
      } else if (node->has_role() &&
                 node->role() == fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH) {
        description.emplace_back(DescribeToggleSwitch(node));
      } else if (node->has_role() &&
                 node->role() == fuchsia::accessibility::semantics::Role::SLIDER) {
        Utterance utterance;
        utterance.set_message(GetSliderLabelAndRangeMessage(node));
        description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)});
      } else if (node->has_attributes() && node->attributes().has_label() &&
                 !node->attributes().label().empty()) {
        Utterance utterance;
        utterance.set_message(node->attributes().label());
  
        // Note that empty descriptions (no labels), are allowed. It is common for developers forget
        // to add accessible labels to their UI elements, which causes them to not have one. It is
        // desirable still to tell the user what the node is (a button). But because our TTS does not
        // support empty utterances we only send the string for "button" in this case.
        description.emplace_back(UtteranceAndContext{.utterance = std::move(utterance)});
      }
    }
    {
      Utterance utterance;
      if (node->has_role()) {
        if (node->role() == Role::BUTTON) {
          description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_BUTTON));
        } else if (node->role() == Role::HEADER) {
          description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_HEADER));
        } else if (node->role() == Role::IMAGE) {
          description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_IMAGE));
        } else if (node->role() == Role::LINK) {
          description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_LINK));
        } else if (node->role() == Role::CHECK_BOX) {
          auto check_box_description = DescribeCheckBox(node);
          std::copy(std::make_move_iterator(check_box_description.begin()),
                    std::make_move_iterator(check_box_description.end()),
                    std::back_inserter(description));
        } else if (node->role() == Role::SLIDER) {
          description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_SLIDER));
        }
      }
    }
    if (NodeIsClickable(node)) {
      description.emplace_back(GenerateUtteranceByMessageId(MessageIds::DOUBLE_TAP_HINT, kLongDelay));
    }
  
    return description;
  }
  
  ScreenReaderMessageGenerator::UtteranceAndContext
  ScreenReaderMessageGenerator::GenerateUtteranceByMessageId(
      MessageIds message_id, zx::duration delay, const std::vector<std::string>& arg_names,
      const std::vector<std::string>& arg_values) {
    UtteranceAndContext utterance;
    auto message = message_formatter_->FormatStringById(static_cast<uint64_t>(message_id), arg_names,
                                                        arg_values);
    if (message != std::nullopt) {
      utterance.utterance.set_message(std::move(*message));
      utterance.delay = delay;
    }
    return utterance;
  }
  
  ScreenReaderMessageGenerator::UtteranceAndContext ScreenReaderMessageGenerator::DescribeRadioButton(
      const fuchsia::accessibility::semantics::Node* node) {
    FX_DCHECK(node->has_role() &&
              node->role() == fuchsia::accessibility::semantics::Role::RADIO_BUTTON);
    const auto message_id =
        node->has_states() && node->states().has_selected() && node->states().selected()
            ? MessageIds::RADIO_BUTTON_SELECTED
            : MessageIds::RADIO_BUTTON_UNSELECTED;
    const auto name_value =
        node->has_attributes() && node->attributes().has_label() ? node->attributes().label() : "";
    if (!name_value.empty()) {
      return GenerateUtteranceByMessageId(message_id, zx::duration(zx::msec(0)), {"name"},
                                          {name_value});
    }
  
    return GenerateUtteranceByMessageId(message_id);
  }
  
  std::vector<ScreenReaderMessageGenerator::UtteranceAndContext>
  ScreenReaderMessageGenerator::DescribeCheckBox(
      const fuchsia::accessibility::semantics::Node* node) {
    FX_DCHECK(node->has_role() && node->role() == fuchsia::accessibility::semantics::Role::CHECK_BOX);
    std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> description;
    description.emplace_back(GenerateUtteranceByMessageId(MessageIds::ROLE_CHECKBOX, kDefaultDelay));
    if (node->has_states() && node->states().has_checked_state() &&
        node->states().checked_state() != fuchsia::accessibility::semantics::CheckedState::NONE) {
      MessageIds message_id = MessageIds::ELEMENT_NOT_CHECKED;
      switch (node->states().checked_state()) {
        case fuchsia::accessibility::semantics::CheckedState::CHECKED:
          message_id = MessageIds::ELEMENT_CHECKED;
          break;
        case fuchsia::accessibility::semantics::CheckedState::UNCHECKED:
          message_id = MessageIds::ELEMENT_NOT_CHECKED;
          break;
        case fuchsia::accessibility::semantics::CheckedState::MIXED:
          message_id = MessageIds::ELEMENT_PARTIALLY_CHECKED;
          break;
        case fuchsia::accessibility::semantics::CheckedState::NONE:
          // When none is present, return without a description of the state.
          return description;
      }
      description.emplace_back(GenerateUtteranceByMessageId(message_id));
    }
    return description;
  }
  
  ScreenReaderMessageGenerator::UtteranceAndContext
  ScreenReaderMessageGenerator::DescribeToggleSwitch(
      const fuchsia::accessibility::semantics::Node* node) {
    FX_DCHECK(node->has_role() &&
              node->role() == fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH);
    const auto message_id =
        node->has_states() && node->states().has_toggled_state() &&
                node->states().toggled_state() == fuchsia::accessibility::semantics::ToggledState::ON
            ? MessageIds::ELEMENT_TOGGLED_ON
            : MessageIds::ELEMENT_TOGGLED_OFF;
    const auto name_value =
        node->has_attributes() && node->attributes().has_label() ? node->attributes().label() : "";
  
    if (!name_value.empty()) {
      return GenerateUtteranceByMessageId(message_id, zx::duration(zx::msec(0)), {"name"},
                                          {name_value});
    }
    return GenerateUtteranceByMessageId(message_id);
  }
  
  }  // namespace a11y
  