chromium/chrome/browser/ash/accessibility/service/accessibility_service_client.cc

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

#include "chrome/browser/ash/accessibility/service/accessibility_service_client.h"

#include <memory>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/uuid.h"
#include "chrome/browser/accessibility/service/accessibility_service_router.h"
#include "chrome/browser/accessibility/service/accessibility_service_router_factory.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/service/accessibility_service_devtools_delegate.h"
#include "chrome/browser/ash/accessibility/service/autoclick_client_impl.h"
#include "chrome/browser/ash/accessibility/service/automation_client_impl.h"
#include "chrome/browser/ash/accessibility/service/speech_recognition_impl.h"
#include "chrome/browser/ash/accessibility/service/tts_client_impl.h"
#include "chrome/browser/ash/accessibility/service/user_input_impl.h"
#include "chrome/browser/ash/accessibility/service/user_interface_impl.h"
#include "chrome/common/chrome_paths.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_agent_host.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "services/accessibility/public/mojom/accessibility_service.mojom.h"

namespace ash {
namespace {

const char kAccessibilityCommonFilesPath[] = "chromeos/accessibility";

base::File LoadFile(base::FilePath path) {
  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  base::FilePath resources_path;
  if (!base::PathService::Get(chrome::DIR_RESOURCES, &resources_path)) {
    NOTREACHED_IN_MIGRATION();
  }

  base::FilePath accessibility_file_path =
      resources_path.Append(kAccessibilityCommonFilesPath).Append(path);
  base::File file(accessibility_file_path,
                  base::File::FLAG_OPEN | base::File::FLAG_READ);
  return file;
}

}  // namespace

AccessibilityServiceClient::AccessibilityServiceClient() = default;

AccessibilityServiceClient::~AccessibilityServiceClient() {
  Reset();
}

void AccessibilityServiceClient::BindAutomation(
    mojo::PendingAssociatedRemote<ax::mojom::Automation> automation) {
  automation_client_->BindAutomation(std::move(automation));
}

void AccessibilityServiceClient::BindAutomationClient(
    mojo::PendingReceiver<ax::mojom::AutomationClient> automation_client) {
  automation_client_->BindAutomationClient(std::move(automation_client));
}

void AccessibilityServiceClient::BindAutoclickClient(
    mojo::PendingReceiver<ax::mojom::AutoclickClient> autoclick_receiver) {
  autoclick_client_->Bind(std::move(autoclick_receiver));
}

void AccessibilityServiceClient::BindSpeechRecognition(
    mojo::PendingReceiver<ax::mojom::SpeechRecognition> sr_receiver) {
  speech_recognition_impl_->Bind(std::move(sr_receiver));
}

void AccessibilityServiceClient::BindTts(
    mojo::PendingReceiver<ax::mojom::Tts> tts_receiver) {
  tts_client_->Bind(std::move(tts_receiver));
}

void AccessibilityServiceClient::BindUserInput(
    mojo::PendingReceiver<ax::mojom::UserInput> ui_receiver) {
  user_input_client_->Bind(std::move(ui_receiver));
}

void AccessibilityServiceClient::BindUserInterface(
    mojo::PendingReceiver<ax::mojom::UserInterface> ui_receiver) {
  user_interface_client_->Bind(std::move(ui_receiver));
}

void AccessibilityServiceClient::BindAccessibilityFileLoader(
    mojo::PendingReceiver<ax::mojom::AccessibilityFileLoader>
        file_loader_receiver) {
  CHECK(!file_loader_.is_bound());
  file_loader_.Bind(std::move(file_loader_receiver));
}

void AccessibilityServiceClient::Load(const base::FilePath& path,
                                      LoadCallback callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()}, base::BindOnce(&LoadFile, path),
      base::BindOnce(&AccessibilityServiceClient::OnFileLoaded,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void AccessibilityServiceClient::SetProfile(content::BrowserContext* profile) {
  // If the profile has changed we will need to disconnect from the previous
  // service, get the service keyed to this profile, and if any features were
  // enabled, re-establish the service connection with those features. Note that
  // this matches behavior in AccessibilityExtensionLoader::SetProfile, which
  // does the parallel logic with the extension system.
  if (profile_ == profile)
    return;

  Reset();
  profile_ = profile;
  if (profile_ && enabled_features_.size())
    LaunchAccessibilityServiceAndBind();
}

void AccessibilityServiceClient::SetChromeVoxEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kChromeVox,
                            enabled);
}

void AccessibilityServiceClient::SetSelectToSpeakEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kSelectToSpeak,
                            enabled);
}

void AccessibilityServiceClient::SetSwitchAccessEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kSwitchAccess,
                            enabled);
}

void AccessibilityServiceClient::SetAutoclickEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kAutoClick,
                            enabled);
}

void AccessibilityServiceClient::SetMagnifierEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kMagnifier,
                            enabled);
}

void AccessibilityServiceClient::SetDictationEnabled(bool enabled) {
  EnableAssistiveTechnology(ax::mojom::AssistiveTechnologyType::kDictation,
                            enabled);
}

void AccessibilityServiceClient::RequestScrollableBoundsForPoint(
    const gfx::Point& point) {
  autoclick_client_->RequestScrollableBoundsForPoint(point);
}

void AccessibilityServiceClient::Reset() {
  at_controller_.reset();
  autoclick_client_.reset();
  file_loader_.reset();
  automation_client_.reset();
  devtools_agent_hosts_.clear();
  speech_recognition_impl_.reset();
  tts_client_.reset();
  user_input_client_.reset();
  user_interface_client_.reset();
}

void AccessibilityServiceClient::EnableAssistiveTechnology(
    ax::mojom::AssistiveTechnologyType type,
    bool enabled) {
  // Update the list of enabled features.
  auto iter =
      std::find(enabled_features_.begin(), enabled_features_.end(), type);
  // If a feature's state isn't being changed, do nothing.
  if ((enabled && iter != enabled_features_.end()) ||
      (!enabled && iter == enabled_features_.end())) {
    return;
  } else if (enabled && iter == enabled_features_.end()) {
    enabled_features_.push_back(type);
  } else if (!enabled && iter != enabled_features_.end()) {
    enabled_features_.erase(iter);
    AccessibilityManager::Get()->RemoveFocusRings(type);
  }

  // If nothing at all is enabled, ensure that automation gets disabled,
  // which will keep the system from collecting and passing a11y trees.
  // Note it is safe to call Disable multiple times in a row.
  if (enabled_features_.empty()) {
    automation_client_->Disable();
  }

  if (!enabled && !at_controller_.is_bound()) {
    // No need to launch the service, nothing is enabled.
    return;
  }

  if (at_controller_.is_bound()) {
    at_controller_->EnableAssistiveTechnology(enabled_features_);
    // Create or destroy devtools agent.
    if (enabled) {
      CreateDevToolsAgentHost(type);
    } else {
      auto it = devtools_agent_hosts_.find(type);
      if (it != devtools_agent_hosts_.end()) {
        // Detach all sessions before destroying.
        it->second->ForceDetachAllSessions();
        devtools_agent_hosts_.erase(it);
      }
    }
    return;
  }

  // A new feature is enabled but the service isn't running yet.
  LaunchAccessibilityServiceAndBind();
}

void AccessibilityServiceClient::LaunchAccessibilityServiceAndBind() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!profile_)
    return;

  ax::AccessibilityServiceRouter* router =
      ax::AccessibilityServiceRouterFactory::GetForBrowserContext(
          static_cast<content::BrowserContext*>(profile_));

  if (!router) {
    return;
  }

  autoclick_client_ = std::make_unique<AutoclickClientImpl>();
  automation_client_ = std::make_unique<AutomationClientImpl>();
  speech_recognition_impl_ = std::make_unique<SpeechRecognitionImpl>(profile_);
  tts_client_ = std::make_unique<TtsClientImpl>(profile_);
  user_input_client_ = std::make_unique<UserInputImpl>();
  user_interface_client_ = std::make_unique<UserInterfaceImpl>();

  // Bind the AXServiceClient before enabling features.
  router->BindAccessibilityServiceClient(
      service_client_.BindNewPipeAndPassRemote());
  router->BindAssistiveTechnologyController(
      at_controller_.BindNewPipeAndPassReceiver(), enabled_features_);
  // Create agent host for all enabled features.
  for (auto& type : enabled_features_) {
    CreateDevToolsAgentHost(type);
  }
}

void AccessibilityServiceClient::CreateDevToolsAgentHost(
    ax::mojom::AssistiveTechnologyType type) {
  auto host = content::DevToolsAgentHost::CreateForMojomDelegate(
      base::Uuid::GenerateRandomV4().AsLowercaseString(),
      // base::Unretained is safe because all agent hosts and
      // their delegates are deleted in the destructor of this class when
      // |hosts_| is cleared.
      std::make_unique<AccessibilityServiceDevToolsDelegate>(
          type,
          base::BindRepeating(&AccessibilityServiceClient::ConnectDevToolsAgent,
                              base::Unretained(this))));
  devtools_agent_hosts_.emplace(type, host);
}

void AccessibilityServiceClient::ConnectDevToolsAgent(
    ::mojo::PendingAssociatedReceiver<::blink::mojom::DevToolsAgent> agent,
    ax::mojom::AssistiveTechnologyType type) {
  if (!profile_) {
    return;
  }

  ax::AccessibilityServiceRouter* router =
      ax::AccessibilityServiceRouterFactory::GetForBrowserContext(
          static_cast<content::BrowserContext*>(profile_));
  if (router) {
    router->ConnectDevToolsAgent(std::move(agent), type);
  }
}

void AccessibilityServiceClient::OnFileLoaded(LoadCallback callback,
                                              base::File file) {
  std::move(callback).Run(std::move(file));
}

}  // namespace ash