chromium/chrome/browser/speech/extension_api/tts_engine_extension_observer_chromeos.cc

// Copyright 2014 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/speech/extension_api/tts_engine_extension_observer_chromeos.h"

#include "base/check.h"
#include "base/memory/singleton.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
#include "chrome/browser/speech/extension_api/tts_engine_extension_api.h"
#include "chrome/common/extensions/api/speech/tts_engine_manifest_handler.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chromeos/services/tts/public/mojom/tts_service.mojom.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/browser/tts_controller.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/common/permissions/permissions_data.h"

namespace {

using ::ash::AccessibilityManager;
using ::ash::AccessibilityNotificationType;

void UpdateGoogleSpeechSynthesisKeepAliveCountHelper(
    content::BrowserContext* context,
    bool increment) {
  extensions::ProcessManager* pm = extensions::ProcessManager::Get(context);
  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(context);

  const extensions::Extension* extension =
      registry->enabled_extensions().GetByID(
          extension_misc::kGoogleSpeechSynthesisExtensionId);
  if (!extension)
    return;

  if (increment) {
    pm->IncrementLazyKeepaliveCount(
        extension, extensions::Activity::ACCESSIBILITY, std::string());
  } else {
    pm->DecrementLazyKeepaliveCount(
        extension, extensions::Activity::ACCESSIBILITY, std::string());
  }
}

void UpdateGoogleSpeechSynthesisKeepAliveCount(content::BrowserContext* context,
                                               bool increment) {
  // Deal with profiles that are non-off the record and otr. For a given
  // extension load/unload, we only ever get called for one of the two potential
  // profile types.
  Profile* profile = Profile::FromBrowserContext(context);
  if (!profile)
    return;

  UpdateGoogleSpeechSynthesisKeepAliveCountHelper(
      profile->HasPrimaryOTRProfile()
          ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
          : profile,
      increment);
}

void UpdateGoogleSpeechSynthesisKeepAliveCountOnReload(
    content::BrowserContext* browser_context) {
  if (AccessibilityManager::Get()->IsSpokenFeedbackEnabled()) {
    UpdateGoogleSpeechSynthesisKeepAliveCount(browser_context,
                                              true /* increment */);
  }

  if (AccessibilityManager::Get()->IsSelectToSpeakEnabled()) {
    UpdateGoogleSpeechSynthesisKeepAliveCount(browser_context,
                                              true /* increment */);
  }
}

}  // namespace

// Factory to load one instance of TtsExtensionLoaderChromeOs per profile.
class TtsEngineExtensionObserverChromeOSFactory
    : public ProfileKeyedServiceFactory {
 public:
  static TtsEngineExtensionObserverChromeOS* GetForProfile(Profile* profile) {
    return static_cast<TtsEngineExtensionObserverChromeOS*>(
        GetInstance()->GetServiceForBrowserContext(profile, true));
  }

  static TtsEngineExtensionObserverChromeOSFactory* GetInstance() {
    return base::Singleton<TtsEngineExtensionObserverChromeOSFactory>::get();
  }

 private:
  friend struct base::DefaultSingletonTraits<
      TtsEngineExtensionObserverChromeOSFactory>;

  TtsEngineExtensionObserverChromeOSFactory()
      : ProfileKeyedServiceFactory(
            "TtsEngineExtensionObserverChromeOS",
            // If given an incognito profile (including the Chrome OS login
            // profile), share the service with the original profile.
            ProfileSelections::Builder()
                .WithRegular(ProfileSelection::kRedirectedToOriginal)
                // TODO(crbug.com/40257657): Check if this service is needed in
                // Guest mode.
                .WithGuest(ProfileSelection::kRedirectedToOriginal)
                // TODO(crbug.com/41488885): Check if this service is needed for
                // Ash Internals.
                .WithAshInternals(ProfileSelection::kRedirectedToOriginal)
                .Build()) {
    DependsOn(extensions::EventRouterFactory::GetInstance());
  }

  ~TtsEngineExtensionObserverChromeOSFactory() override {}

  KeyedService* BuildServiceInstanceFor(
      content::BrowserContext* profile) const override {
    return new TtsEngineExtensionObserverChromeOS(
        static_cast<Profile*>(profile));
  }
};

TtsEngineExtensionObserverChromeOS*
TtsEngineExtensionObserverChromeOS::GetInstance(Profile* profile) {
  return TtsEngineExtensionObserverChromeOSFactory::GetInstance()
      ->GetForProfile(profile);
}

TtsEngineExtensionObserverChromeOS::TtsEngineExtensionObserverChromeOS(
    Profile* profile)
    : profile_(profile) {
  extension_registry_observation_.Observe(
      extensions::ExtensionRegistry::Get(profile_));

  extensions::EventRouter* event_router =
      extensions::EventRouter::Get(profile_);
  DCHECK(event_router);
  event_router->RegisterObserver(this, tts_engine_events::kOnSpeak);
  event_router->RegisterObserver(this, tts_engine_events::kOnStop);

  accessibility_status_subscription_ =
      AccessibilityManager::Get()->RegisterCallback(base::BindRepeating(
          &TtsEngineExtensionObserverChromeOS::OnAccessibilityStatusChanged,
          base::Unretained(this)));
}

TtsEngineExtensionObserverChromeOS::~TtsEngineExtensionObserverChromeOS() =
    default;

void TtsEngineExtensionObserverChromeOS::BindGoogleTtsStream(
    mojo::PendingReceiver<chromeos::tts::mojom::GoogleTtsStream> receiver) {
  // At this point, the component extension has loaded, and the js has requested
  // a TtsStreamFactory be bound. It's safe now to update the keep alive count
  // for important accessibility features. This path is also encountered if the
  // component extension background page forceably window.close(s) on error.
  UpdateGoogleSpeechSynthesisKeepAliveCountOnReload(profile_);

  CreateTtsServiceIfNeeded();

  // Always create a new audio stream for the tts stream. It is assumed once the
  // tts stream is reset by the service, the audio stream is appropriately
  // cleaned up by the audio service.
  mojo::PendingRemote<media::mojom::AudioStreamFactory> factory_remote;
  auto factory_receiver = factory_remote.InitWithNewPipeAndPassReceiver();
  content::GetAudioService().BindStreamFactory(std::move(factory_receiver));
  tts_service_->BindGoogleTtsStream(std::move(receiver),
                                    std::move(factory_remote));
}

void TtsEngineExtensionObserverChromeOS::BindPlaybackTtsStream(
    mojo::PendingReceiver<chromeos::tts::mojom::PlaybackTtsStream> receiver,
    chromeos::tts::mojom::AudioParametersPtr audio_parameters,
    chromeos::tts::mojom::TtsService::BindPlaybackTtsStreamCallback callback) {
  CreateTtsServiceIfNeeded();

  // Always create a new audio stream for the tts stream. It is assumed once the
  // tts stream is reset by the service, the audio stream is appropriately
  // cleaned up by the audio service.
  mojo::PendingRemote<media::mojom::AudioStreamFactory> factory_remote;
  auto factory_receiver = factory_remote.InitWithNewPipeAndPassReceiver();
  content::GetAudioService().BindStreamFactory(std::move(factory_receiver));
  tts_service_->BindPlaybackTtsStream(
      std::move(receiver), std::move(factory_remote),
      std::move(audio_parameters), std::move(callback));
}

void TtsEngineExtensionObserverChromeOS::Shutdown() {
  extensions::EventRouter::Get(profile_)->UnregisterObserver(this);
}

bool TtsEngineExtensionObserverChromeOS::IsLoadedTtsEngine(
    const std::string& extension_id) {
  extensions::EventRouter* event_router =
      extensions::EventRouter::Get(profile_);
  DCHECK(event_router);
  if ((event_router->ExtensionHasEventListener(extension_id,
                                               tts_engine_events::kOnSpeak) ||
       event_router->ExtensionHasEventListener(
           extension_id, tts_engine_events::kOnSpeakWithAudioStream)) &&
      event_router->ExtensionHasEventListener(extension_id,
                                              tts_engine_events::kOnStop)) {
    return true;
  }

  return false;
}

void TtsEngineExtensionObserverChromeOS::OnListenerAdded(
    const extensions::EventListenerInfo& details) {
  if (!IsLoadedTtsEngine(details.extension_id))
    return;

  content::TtsController::GetInstance()->VoicesChanged();
}

void TtsEngineExtensionObserverChromeOS::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension) {
  // TODO(jennyz): Do we need to monitor this in Lacros for loading 3rd party
  // tts engine extensions?
  if (extension->permissions_data()->HasAPIPermission(
          extensions::mojom::APIPermissionID::kTtsEngine)) {
    engine_extension_ids_.insert(extension->id());

    if (extension->id() == extension_misc::kGoogleSpeechSynthesisExtensionId)
      UpdateGoogleSpeechSynthesisKeepAliveCountOnReload(browser_context);
  }
}

void TtsEngineExtensionObserverChromeOS::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    extensions::UnloadedExtensionReason reason) {
  size_t erase_count = 0;
  erase_count += engine_extension_ids_.erase(extension->id());
  if (erase_count > 0)
    content::TtsController::GetInstance()->VoicesChanged();

  if (tts_service_ &&
      extension->id() == extension_misc::kGoogleSpeechSynthesisExtensionId)
    tts_service_.reset();
}

void TtsEngineExtensionObserverChromeOS::OnAccessibilityStatusChanged(
    const ash::AccessibilityStatusEventDetails& details) {
  if (details.notification_type !=
          AccessibilityNotificationType::kToggleSpokenFeedback &&
      details.notification_type !=
          AccessibilityNotificationType::kToggleSelectToSpeak) {
    return;
  }

  // Google speech synthesis might not be loaded yet. If it isn't, the call in
  // |OnExtensionLoaded| will do the increment. If it is, the call below will
  // increment. Decrements only occur when toggling off here.
  UpdateGoogleSpeechSynthesisKeepAliveCount(profile(), details.enabled);
}

void TtsEngineExtensionObserverChromeOS::CreateTtsServiceIfNeeded() {
  // Only launch a new TtsService if necessary. By assigning below, if
  // |tts_service_| held a remote, it will be killed and a new one created,
  // ensuring we only ever have one TtsService running.
  if (tts_service_)
    return;

  tts_service_ =
      content::ServiceProcessHost::Launch<chromeos::tts::mojom::TtsService>(
          content::ServiceProcessHost::Options()
              .WithDisplayName("TtsService")
              .Pass());

  tts_service_.set_disconnect_handler(base::BindOnce(
      [](mojo::Remote<chromeos::tts::mojom::TtsService>* tts_service) {
        tts_service->reset();
      },
      &tts_service_));
}

// static
void TtsEngineExtensionObserverChromeOS::EnsureFactoryBuilt() {
  TtsEngineExtensionObserverChromeOSFactory::GetInstance();
}