910e62b5创建于 1月15日历史提交
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/dbus/menu/menu.h"

#include <limits>
#include <memory>
#include <set>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/rtl.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "components/dbus/properties/dbus_properties.h"
#include "components/dbus/properties/success_barrier_callback.h"
#include "components/dbus/utils/variant.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/models/menu_model.h"
#include "ui/menus/simple_menu_model.h"

namespace {

// Interfaces.
const char kInterfaceDbusMenu[] = "com.canonical.dbusmenu";

// Methods.
const char kMethodAboutToShow[] = "AboutToShow";
const char kMethodAboutToShowGroup[] = "AboutToShowGroup";
const char kMethodEvent[] = "Event";
const char kMethodEventGroup[] = "EventGroup";
const char kMethodGetGroupProperties[] = "GetGroupProperties";
const char kMethodGetLayout[] = "GetLayout";
const char kMethodGetProperty[] = "GetProperty";

// Properties.
const char kPropertyIconThemePath[] = "IconThemePath";
const char kPropertyMenuStatus[] = "Status";
const char kPropertyTextDirection[] = "TextDirection";
const char kPropertyVersion[] = "Version";

// Property values.
const char kPropertyValueStatusNormal[] = "normal";
constexpr uint32_t kPropertyValueVersion = 3;

// Signals.
const char kSignalItemsPropertiesUpdated[] = "ItemsPropertiesUpdated";
const char kSignalLayoutUpdated[] = "LayoutUpdated";

// Creates a variant with the default value for |property_name|, or an empty
// variant if |property_name| is invalid.
dbus_utils::Variant CreateDefaultPropertyValue(
    const std::string& property_name) {
  if (property_name == "type") {
    return dbus_utils::Variant::Wrap<"s">("standard");
  }
  if (property_name == "label") {
    return dbus_utils::Variant::Wrap<"s">("");
  }
  if (property_name == "enabled") {
    return dbus_utils::Variant::Wrap<"b">(true);
  }
  if (property_name == "visible") {
    return dbus_utils::Variant::Wrap<"b">(true);
  }
  if (property_name == "icon-name") {
    return dbus_utils::Variant::Wrap<"s">("");
  }
  if (property_name == "icon-data") {
    return dbus_utils::Variant::Wrap<"ay">(std::vector<uint8_t>());
  }
  if (property_name == "shortcut") {
    return dbus_utils::Variant::Wrap<"aas">(
        std::vector<std::vector<std::string>>());
  }
  if (property_name == "toggle-type") {
    return dbus_utils::Variant::Wrap<"s">("");
  }
  if (property_name == "toggle-state") {
    return dbus_utils::Variant::Wrap<"i">(-1);
  }
  if (property_name == "children-display") {
    return dbus_utils::Variant::Wrap<"s">("");
  }
  return dbus_utils::Variant();
}

const char* DbusTextDirection() {
  return base::i18n::IsRTL() ? "rtl " : "ltr";
}

void WriteRemovedProperties(dbus::MessageWriter* writer,
                            const MenuPropertyChanges& removed_props) {
  dbus::MessageWriter removed_props_writer(nullptr);
  writer->OpenArray("(ias)", &removed_props_writer);
  for (const auto& pair : removed_props) {
    dbus::MessageWriter struct_writer(nullptr);
    removed_props_writer.OpenStruct(&struct_writer);
    struct_writer.AppendInt32(pair.first);
    struct_writer.AppendArrayOfStrings(pair.second);
    removed_props_writer.CloseContainer(&struct_writer);
  }
  writer->CloseContainer(&removed_props_writer);
}

}  // namespace

DbusMenu::MenuItem::MenuItem(int32_t id,
                             MenuItemProperties&& properties,
                             std::vector<int32_t>&& children,
                             ui::MenuModel* menu,
                             ui::MenuModel* containing_menu,
                             size_t containing_menu_index)
    : id(id),
      properties(std::move(properties)),
      children(std::move(children)),
      menu(menu),
      containing_menu(containing_menu),
      containing_menu_index(containing_menu_index) {}

DbusMenu::MenuItem::~MenuItem() = default;

DbusMenu::ScopedMethodResponse::ScopedMethodResponse(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender)
    : method_call_(method_call),
      response_sender_(std::move(response_sender)),
      reader_(method_call_) {}

DbusMenu::ScopedMethodResponse::~ScopedMethodResponse() {
  // Reset method_call_, it may no longer be valid after Run().
  method_call_ = nullptr;

  std::move(response_sender_).Run(std::move(response_));
}

dbus::MessageWriter& DbusMenu::ScopedMethodResponse::Writer() {
  EnsureResponse();
  if (!writer_) {
    writer_ = std::make_unique<dbus::MessageWriter>(response_.get());
  }
  return *writer_;
}

void DbusMenu::ScopedMethodResponse::EnsureResponse() {
  if (!response_) {
    response_ = dbus::Response::FromMethodCall(method_call_);
  }
}

DbusMenu::DbusMenu(dbus::ExportedObject* exported_object,
                   InitializedCallback callback)
    : menu_(exported_object) {
  SetModel(nullptr, false);

  static constexpr struct {
    const char* name;
    void (DbusMenu::*callback)(ScopedMethodResponse*);
  } methods[7] = {
      {kMethodAboutToShow, &DbusMenu::OnAboutToShow},
      {kMethodAboutToShowGroup, &DbusMenu::OnAboutToShowGroup},
      {kMethodEvent, &DbusMenu::OnEvent},
      {kMethodEventGroup, &DbusMenu::OnEventGroup},
      {kMethodGetGroupProperties, &DbusMenu::OnGetGroupProperties},
      {kMethodGetLayout, &DbusMenu::OnGetLayout},
      {kMethodGetProperty, &DbusMenu::OnGetProperty},
  };

  // std::size(methods) calls for method export, 1 call for properties
  // initialization.
  barrier_ =
      SuccessBarrierCallback(std::size(methods) + 1, std::move(callback));
  for (const auto& method : methods) {
    menu_->ExportMethod(
        kInterfaceDbusMenu, method.name,
        WrapMethodCallback(
            base::BindRepeating(method.callback, weak_factory_.GetWeakPtr())),
        base::BindRepeating(&DbusMenu::OnExported, weak_factory_.GetWeakPtr()));
  }

  properties_ = std::make_unique<DbusProperties>(menu_, barrier_);
  properties_->RegisterInterface(kInterfaceDbusMenu);
  properties_->SetProperty<"as">(kInterfaceDbusMenu, kPropertyIconThemePath, {},
                                 false);
  properties_->SetProperty<"s">(kInterfaceDbusMenu, kPropertyMenuStatus,
                                kPropertyValueStatusNormal, false);
  properties_->SetProperty<"s">(kInterfaceDbusMenu, kPropertyTextDirection,
                                DbusTextDirection(), false);
  properties_->SetProperty<"u">(kInterfaceDbusMenu, kPropertyVersion,
                                kPropertyValueVersion, false);
}

DbusMenu::~DbusMenu() = default;

void DbusMenu::SetModel(ui::MenuModel* model, bool send_signal) {
  items_.clear();

  MenuItemProperties properties;
  std::vector<int32_t> children;
  if (model) {
    properties["children-display"] = dbus_utils::Variant::Wrap<"s">("submenu");
    children = ConvertMenu(model);
  }
  items_[0] = std::make_unique<MenuItem>(
      0, std::move(properties), std::move(children), nullptr, nullptr, -1);

  if (send_signal) {
    SendLayoutChangedSignal(0);
  }
}

void DbusMenu::MenuLayoutUpdated(ui::MenuModel* model) {
  MenuItem* item = FindMenuItemForModel(model, items_[0].get());
  DCHECK(item);
  DeleteItemChildren(item);
  item->children = ConvertMenu(model);
  SendLayoutChangedSignal(item->id);
}

void DbusMenu::MenuItemsPropertiesUpdated(
    const std::vector<MenuItemReference>& menu_items) {
  if (menu_items.empty()) {
    return;
  }

  MenuPropertyChanges updated_props;
  MenuPropertyChanges removed_props;
  for (const auto& menu_item : menu_items) {
    ui::MenuModel* menu = menu_item.first;
    size_t index = menu_item.second;
    MenuItem* parent = FindMenuItemForModel(menu, items_[0].get());
    MenuItem* item = nullptr;
    for (int32_t id : parent->children) {
      MenuItem* child = items_[id].get();
      DCHECK_EQ(child->containing_menu, menu);
      if (child->containing_menu_index == index) {
        item = child;
        break;
      }
    }
    DCHECK(item);

    auto old_properties = std::move(item->properties);
    item->properties = ComputeMenuPropertiesForMenuItem(menu, index);
    MenuPropertyList item_updated_props;
    MenuPropertyList item_removed_props;
    ComputeMenuPropertyChanges(old_properties, item->properties,
                               &item_updated_props, &item_removed_props);
    if (!item_updated_props.empty()) {
      updated_props[item->id] = std::move(item_updated_props);
    }
    if (!item_removed_props.empty()) {
      removed_props[item->id] = std::move(item_removed_props);
    }
  }

  dbus::Signal signal(kInterfaceDbusMenu, kSignalItemsPropertiesUpdated);
  dbus::MessageWriter writer(&signal);
  WriteUpdatedProperties(&writer, updated_props);
  WriteRemovedProperties(&writer, removed_props);
  menu_->SendSignal(&signal);
}

// static
dbus::ExportedObject::MethodCallCallback DbusMenu::WrapMethodCallback(
    base::RepeatingCallback<void(ScopedMethodResponse*)> callback) {
  return base::BindRepeating(
      [](base::RepeatingCallback<void(ScopedMethodResponse*)> bound_callback,
         dbus::MethodCall* method_call,
         dbus::ExportedObject::ResponseSender response_sender) {
        ScopedMethodResponse response(method_call, std::move(response_sender));
        bound_callback.Run(&response);
      },
      callback);
}

void DbusMenu::OnExported(const std::string& interface_name,
                          const std::string& method_name,
                          bool success) {
  barrier_.Run(success);
}

void DbusMenu::OnAboutToShow(ScopedMethodResponse* response) {
  int32_t id;
  if (!response->reader().PopInt32(&id)) {
    return;
  }

  if (!AboutToShowImpl(id)) {
    return;
  }

  response->Writer().AppendBool(false);
}

void DbusMenu::OnAboutToShowGroup(ScopedMethodResponse* response) {
  dbus::MessageReader array_reader(nullptr);
  if (!response->reader().PopArray(&array_reader)) {
    return;
  }
  std::vector<int32_t> ids;
  while (array_reader.HasMoreData()) {
    int32_t id;
    if (!array_reader.PopInt32(&id)) {
      return;
    }
    ids.push_back(id);
  }

  std::vector<int32_t> id_errors;
  for (int32_t id : ids) {
    if (!AboutToShowImpl(id)) {
      id_errors.push_back(id);
    }
  }

  // IDs of updates needed (none).
  response->Writer().AppendArrayOfInt32s({});
  // Invalid IDs.
  response->Writer().AppendArrayOfInt32s(id_errors);
}

void DbusMenu::OnEvent(ScopedMethodResponse* response) {
  if (!EventImpl(&response->reader(), nullptr)) {
    return;
  }

  response->EnsureResponse();
}

void DbusMenu::OnEventGroup(ScopedMethodResponse* response) {
  dbus::MessageReader array_reader(nullptr);
  if (!response->reader().PopArray(&array_reader)) {
    return;
  }
  std::vector<int32_t> id_errors;
  while (array_reader.HasMoreData()) {
    dbus::MessageReader struct_reader(nullptr);
    array_reader.PopStruct(&struct_reader);

    int32_t id_error = -1;
    if (!EventImpl(&struct_reader, &id_error)) {
      if (id_error < 0) {
        return;
      }
      id_errors.push_back(id_error);
    }
  }

  response->Writer().AppendArrayOfInt32s(id_errors);
}

void DbusMenu::OnGetGroupProperties(ScopedMethodResponse* response) {
  dbus::MessageReader id_reader(nullptr);
  if (!response->reader().PopArray(&id_reader)) {
    return;
  }
  std::vector<int32_t> ids;
  while (id_reader.HasMoreData()) {
    int32_t id;
    if (!id_reader.PopInt32(&id)) {
      return;
    }
    ids.push_back(id);
  }

  std::set<std::string> property_filter;
  dbus::MessageReader property_reader(nullptr);
  if (!response->reader().PopArray(&property_reader)) {
    return;
  }
  while (property_reader.HasMoreData()) {
    std::string property;
    if (!property_reader.PopString(&property)) {
      return;
    }
    property_filter.insert(property);
  }

  dbus::MessageWriter& writer = response->Writer();
  dbus::MessageWriter item_writer(nullptr);
  writer.OpenArray("(ia{sv})", &item_writer);

  auto write_item = [&](int32_t id, const MenuItem& item) {
    dbus::MessageWriter struct_writer(nullptr);
    item_writer.OpenStruct(&struct_writer);
    struct_writer.AppendInt32(id);
    dbus::MessageWriter property_writer(nullptr);
    struct_writer.OpenArray("{sv}", &property_writer);
    for (const auto& property_pair : item.properties) {
      if (!property_filter.empty() &&
          !base::Contains(property_filter, property_pair.first)) {
        continue;
      }
      dbus::MessageWriter dict_entry_writer(nullptr);
      property_writer.OpenDictEntry(&dict_entry_writer);
      dict_entry_writer.AppendString(property_pair.first);
      property_pair.second.Write(dict_entry_writer);
      property_writer.CloseContainer(&dict_entry_writer);
    }
    struct_writer.CloseContainer(&property_writer);
    item_writer.CloseContainer(&struct_writer);
  };

  if (ids.empty()) {
    for (const auto& item_pair : items_) {
      write_item(item_pair.first, *item_pair.second);
    }
  } else {
    for (int32_t id : ids) {
      auto it = items_.find(id);
      if (it != items_.end()) {
        write_item(id, *it->second);
      }
    }
  }

  writer.CloseContainer(&item_writer);
}

void DbusMenu::OnGetLayout(ScopedMethodResponse* response) {
  dbus::MessageReader& reader = response->reader();
  int32_t id;
  int32_t depth;
  MenuPropertyList property_filter;
  if (!reader.PopInt32(&id) || !reader.PopInt32(&depth) || depth < -1 ||
      !reader.PopArrayOfStrings(&property_filter)) {
    return;
  }

  auto it = items_.find(id);
  if (it == items_.end()) {
    return;
  }

  dbus::MessageWriter& writer = response->Writer();
  writer.AppendUint32(revision_);
  WriteMenuItem(it->second.get(), &writer, depth, property_filter);
}

void DbusMenu::OnGetProperty(ScopedMethodResponse* response) {
  dbus::MessageReader& reader = response->reader();
  int32_t id;
  std::string name;
  if (!reader.PopInt32(&id) || !reader.PopString(&name)) {
    return;
  }

  auto item_it = items_.find(id);
  if (item_it == items_.end()) {
    return;
  }

  MenuItem* item = item_it->second.get();
  auto property_it = item->properties.find(name);
  if (property_it == item->properties.end()) {
    dbus_utils::Variant default_value = CreateDefaultPropertyValue(name);
    if (!default_value.signature().empty()) {
      default_value.Write(response->Writer());
    }
  } else {
    property_it->second.Write(response->Writer());
  }
}

bool DbusMenu::AboutToShowImpl(int32_t id) {
  auto item = items_.find(id);
  if (item == items_.end()) {
    return false;
  }

  if (id != 0) {
    ui::MenuModel* menu = item->second->menu;
    if (!menu) {
      return false;
    }

    menu->MenuWillShow();
  }

  return true;
}

bool DbusMenu::EventImpl(dbus::MessageReader* reader, int32_t* id_error) {
  int32_t id;
  if (!reader->PopInt32(&id)) {
    return false;
  }
  auto item_it = items_.find(id);
  if (item_it == items_.end()) {
    if (id_error) {
      *id_error = id;
    }
    return false;
  }

  std::string type;
  if (!reader->PopString(&type)) {
    return false;
  }

  if (type == "clicked") {
    MenuItem* item = item_it->second.get();
    if (!item->containing_menu) {
      return false;
    }
    item->containing_menu->ActivatedAt(item->containing_menu_index);
  } else {
    DCHECK(type == "hovered" || type == "opened" || type == "closed")
        << "Unexpected type: " << type;
    // Nothing to do.
  }

  return true;
}

std::vector<int32_t> DbusMenu::ConvertMenu(ui::MenuModel* menu) {
  std::vector<int32_t> items;
  if (!menu) {
    return items;
  }
  items.reserve(menu->GetItemCount());

  for (size_t i = 0; i < menu->GetItemCount(); ++i) {
    ui::MenuModel* submenu = menu->GetSubmenuModelAt(i);
    std::vector<int32_t> children = ConvertMenu(submenu);

    int32_t id = NextItemId();
    items_[id] = std::make_unique<MenuItem>(
        id, ComputeMenuPropertiesForMenuItem(menu, i), std::move(children),
        submenu, menu, i);
    items.push_back(id);
  }

  return items;
}

int32_t DbusMenu::NextItemId() {
  return last_item_id_ = last_item_id_ == std::numeric_limits<int32_t>::max()
                             ? 1
                             : last_item_id_ + 1;
}

void DbusMenu::WriteMenuItem(const MenuItem* item,
                             dbus::MessageWriter* writer,
                             int32_t depth,
                             const MenuPropertyList& property_filter) const {
  dbus::MessageWriter struct_writer(nullptr);
  writer->OpenStruct(&struct_writer);
  struct_writer.AppendInt32(item->id);

  dbus::MessageWriter properties_writer(nullptr);
  struct_writer.OpenArray("{sv}", &properties_writer);
  for (const auto& property : item->properties) {
    if (property_filter.empty() ||
        base::Contains(property_filter, property.first)) {
      dbus::MessageWriter dict_entry_writer(nullptr);
      properties_writer.OpenDictEntry(&dict_entry_writer);
      dict_entry_writer.AppendString(property.first);
      property.second.Write(dict_entry_writer);
      properties_writer.CloseContainer(&dict_entry_writer);
    }
  }
  struct_writer.CloseContainer(&properties_writer);

  dbus::MessageWriter children_writer(nullptr);
  struct_writer.OpenArray("v", &children_writer);
  if (depth != 0) {
    for (int32_t child : item->children) {
      dbus::MessageWriter variant_writer(nullptr);
      children_writer.OpenVariant("(ia{sv}av)", &variant_writer);
      WriteMenuItem(items_.find(child)->second.get(), &variant_writer,
                    depth == -1 ? -1 : depth - 1, property_filter);
      children_writer.CloseContainer(&variant_writer);
    }
  }
  struct_writer.CloseContainer(&children_writer);

  writer->CloseContainer(&struct_writer);
}

void DbusMenu::WriteUpdatedProperties(
    dbus::MessageWriter* writer,
    const MenuPropertyChanges& updated_props) const {
  dbus::MessageWriter updated_props_writer(nullptr);
  writer->OpenArray("(ia{sv})", &updated_props_writer);
  for (const auto& pair : updated_props) {
    int32_t id = pair.first;
    MenuItem* item = items_.find(id)->second.get();
    dbus::MessageWriter struct_writer(nullptr);
    updated_props_writer.OpenStruct(&struct_writer);
    struct_writer.AppendInt32(id);
    dbus::MessageWriter array_writer(nullptr);
    struct_writer.OpenArray("{sv}", &array_writer);
    for (const std::string& key : pair.second) {
      dbus::MessageWriter dict_entry_writer(nullptr);
      array_writer.OpenDictEntry(&dict_entry_writer);
      dict_entry_writer.AppendString(key);
      item->properties[key].Write(dict_entry_writer);
      array_writer.CloseContainer(&dict_entry_writer);
    }
    struct_writer.CloseContainer(&array_writer);
    updated_props_writer.CloseContainer(&struct_writer);
  }
  writer->CloseContainer(&updated_props_writer);
}

DbusMenu::MenuItem* DbusMenu::FindMenuItemForModel(const ui::MenuModel* model,
                                                   MenuItem* item) const {
  if (item->menu == model) {
    return item;
  }
  for (int32_t id : item->children) {
    MenuItem* child = items_.find(id)->second.get();
    MenuItem* found = FindMenuItemForModel(model, child);
    if (found) {
      return found;
    }
  }
  return nullptr;
}

void DbusMenu::DeleteItem(MenuItem* item) {
  DeleteItemChildren(item);
  items_.erase(item->id);
}

void DbusMenu::DeleteItemChildren(MenuItem* item) {
  for (int32_t id : item->children) {
    DeleteItem(items_.find(id)->second.get());
  }
}

void DbusMenu::SendLayoutChangedSignal(int32_t id) {
  dbus::Signal signal(kInterfaceDbusMenu, kSignalLayoutUpdated);
  dbus::MessageWriter writer(&signal);
  writer.AppendUint32(++revision_);  // Revision of the new layout.
  writer.AppendInt32(id);            // Parent item whose layout changed.
  menu_->SendSignal(&signal);
}