// 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 "chrome/browser/ui/ash/projector/projector_client_impl.h"
#include <optional>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/projector/projector_metrics.h"
#include "ash/public/cpp/projector/projector_controller.h"
#include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
#include "ash/webui/projector_app/projector_app_client.h"
#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/speech/speech_recognition_recognizer_client_impl.h"
#include "chrome/browser/ui/ash/projector/projector_utils.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "components/soda/soda_installer.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "media/audio/audio_device_description.h"
#include "media/base/media_switches.h"
#include "media/mojo/mojom/speech_recognition_service.mojom.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "url/gurl.h"
namespace {
constexpr char kUSMExperimentRoutingId[] = "screencast_usm_rnnt";
inline const std::string& GetLocale() {
return g_browser_process->GetApplicationLocale();
}
inline const std::string GetLocaleOrLanguageForServerSideRecognition() {
const std::string& locale = g_browser_process->GetApplicationLocale();
// Some languages and locales need to be mapped to the default
// languages/locales provided by the server side speech recognition service.
if (locale == "ar") {
return "ar-x-maghrebi";
}
return locale;
}
ash::OnDeviceToServerSpeechRecognitionFallbackReason GetFallbackReason(
ash::OnDeviceRecognitionAvailability availability) {
if (ash::features::ShouldForceEnableServerSideSpeechRecognitionForDev()) {
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kEnforcedByFlag;
}
DCHECK_NE(availability, ash::OnDeviceRecognitionAvailability::kAvailable);
switch (availability) {
case ash::OnDeviceRecognitionAvailability::kSodaNotAvailable:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kSodaNotAvailable;
case ash::OnDeviceRecognitionAvailability::kUserLanguageNotAvailable:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kUserLanguageNotAvailableForSoda;
case ash::OnDeviceRecognitionAvailability::kSodaNotInstalled:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kSodaNotInstalled;
case ash::OnDeviceRecognitionAvailability::kSodaInstalling:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kSodaInstalling;
case ash::OnDeviceRecognitionAvailability::
kSodaInstallationErrorUnspecified:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kSodaInstallationErrorUnspecified;
case ash::OnDeviceRecognitionAvailability::
kSodaInstallationErrorNeedsReboot:
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::
kSodaInstallationErrorNeedsReboot;
case ash::OnDeviceRecognitionAvailability::kAvailable:
break;
}
NOTREACHED_IN_MIGRATION();
return ash::OnDeviceToServerSpeechRecognitionFallbackReason::kMaxValue;
}
} // namespace
// Using base::Unretained for callback is safe since the ProjectorClientImpl
// owns `drive_helper_`.
ProjectorClientImpl::ProjectorClientImpl(ash::ProjectorController* controller)
: controller_(controller),
drive_helper_(base::BindRepeating(
&ProjectorClientImpl::MaybeSwitchDriveIntegrationServiceObservation,
base::Unretained(this))) {
controller_->SetClient(this);
session_manager::SessionManager* session_manager =
session_manager::SessionManager::Get();
if (session_manager) {
session_observation_.Observe(session_manager);
}
if (base::FeatureList::IsEnabled(ash::features::kOnDeviceSpeechRecognition)) {
soda_installation_controller_ =
std::make_unique<ProjectorSodaInstallationController>(
ash::ProjectorAppClient::Get(), controller_);
}
}
ProjectorClientImpl::ProjectorClientImpl()
: ProjectorClientImpl(ash::ProjectorController::Get()) {}
ProjectorClientImpl::~ProjectorClientImpl() {
controller_->SetClient(nullptr);
}
// Projector prioritizes on-device speech recognition over server
// based speech recognition.
ash::SpeechRecognitionAvailability
ProjectorClientImpl::GetSpeechRecognitionAvailability() const {
ash::SpeechRecognitionAvailability availability;
availability.use_on_device = true;
availability.on_device_availability = SpeechRecognitionRecognizerClientImpl::
GetOnDeviceSpeechRecognitionAvailability(GetLocale());
availability.server_based_availability =
SpeechRecognitionRecognizerClientImpl::
GetServerBasedRecognitionAvailability(
GetLocaleOrLanguageForServerSideRecognition());
if (ash::features::ShouldForceEnableServerSideSpeechRecognitionForDev() ||
(availability.on_device_availability !=
ash::OnDeviceRecognitionAvailability::kAvailable &&
availability.server_based_availability ==
ash::ServerBasedRecognitionAvailability::kAvailable)) {
availability.use_on_device = false;
}
return availability;
}
void ProjectorClientImpl::StartSpeechRecognition() {
const auto availability = GetSpeechRecognitionAvailability();
DCHECK(availability.IsAvailable());
DCHECK_EQ(speech_recognizer_.get(), nullptr);
recognizer_status_ = SPEECH_RECOGNIZER_OFF;
const std::string locale =
availability.use_on_device
? GetLocale()
: GetLocaleOrLanguageForServerSideRecognition();
const std::string experiment_recognizer_routing_key =
ash::features::IsProjectorUseUSMForS3Enabled() ? kUSMExperimentRoutingId
: "";
speech_recognizer_ = std::make_unique<SpeechRecognitionRecognizerClientImpl>(
weak_ptr_factory_.GetWeakPtr(), ProfileManager::GetActiveUserProfile(),
media::AudioDeviceDescription::kDefaultDeviceId,
media::mojom::SpeechRecognitionOptions::New(
media::mojom::SpeechRecognitionMode::kCaption,
/*enable_formatting=*/true, locale,
/*is_server_based=*/!availability.use_on_device,
media::mojom::RecognizerClientType::kProjector,
/*skip_continuously_empty_audio=*/false,
experiment_recognizer_routing_key));
if (!availability.use_on_device) {
RecordOnDeviceToServerSpeechRecognitionFallbackReason(
GetFallbackReason(availability.on_device_availability));
}
}
void ProjectorClientImpl::StopSpeechRecognition() {
if (!speech_recognizer_) {
LOG(ERROR) << "Stop was called on a destroyed speech recognizer.";
return;
}
speech_recognizer_->Stop();
}
void ProjectorClientImpl::ForceEndSpeechRecognition() {
SpeechRecognitionEnded(/*forced=*/true);
}
bool ProjectorClientImpl::GetBaseStoragePath(base::FilePath* result) const {
if (!IsDriveFsMounted()) {
return false;
}
if (ash::ProjectorController::AreExtendedProjectorFeaturesDisabled()) {
auto* profile = ProfileManager::GetActiveUserProfile();
DCHECK(profile);
DownloadPrefs* download_prefs = DownloadPrefs::FromBrowserContext(
ProfileManager::GetActiveUserProfile());
*result = download_prefs->GetDefaultDownloadDirectoryForProfile();
return true;
}
*result = ProjectorDriveFsProvider::GetDriveFsMountPointPath();
return true;
}
bool ProjectorClientImpl::IsDriveFsMounted() const {
if (!ash::LoginState::Get()->IsUserLoggedIn()) {
return false;
}
if (ash::ProjectorController::AreExtendedProjectorFeaturesDisabled()) {
// Return true when extended projector features are disabled. Use download
// folder for Projector storage.
return true;
}
return ProjectorDriveFsProvider::IsDriveFsMounted();
}
bool ProjectorClientImpl::IsDriveFsMountFailed() const {
return ProjectorDriveFsProvider::IsDriveFsMountFailed();
}
void ProjectorClientImpl::OpenProjectorApp() const {
auto* profile = ProfileManager::GetActiveUserProfile();
ash::LaunchSystemWebAppAsync(profile, ash::SystemWebAppType::PROJECTOR);
}
void ProjectorClientImpl::MinimizeProjectorApp() const {
auto* profile = ProfileManager::GetActiveUserProfile();
auto* browser =
ash::FindSystemWebAppBrowser(profile, ash::SystemWebAppType::PROJECTOR);
if (browser) {
browser->window()->Minimize();
}
}
void ProjectorClientImpl::CloseProjectorApp() const {
auto* profile = ProfileManager::GetActiveUserProfile();
auto* browser =
ash::FindSystemWebAppBrowser(profile, ash::SystemWebAppType::PROJECTOR);
if (browser) {
browser->window()->Close();
}
}
void ProjectorClientImpl::OnNewScreencastPreconditionChanged(
const ash::NewScreencastPrecondition& precondition) const {
ash::ProjectorAppClient* app_client = ash::ProjectorAppClient::Get();
if (app_client) {
app_client->OnNewScreencastPreconditionChanged(precondition);
}
}
void ProjectorClientImpl::ToggleFileSyncingNotificationForPaths(
const std::vector<base::FilePath>& screencast_paths,
bool suppress) {
if (auto* app_client = ash::ProjectorAppClient::Get()) {
app_client->ToggleFileSyncingNotificationForPaths(screencast_paths,
suppress);
}
}
void ProjectorClientImpl::OnSpeechResult(
const std::u16string& text,
bool is_final,
const std::optional<media::SpeechRecognitionResult>& full_result) {
DCHECK(full_result.has_value());
controller_->OnTranscription(full_result.value());
}
void ProjectorClientImpl::OnSpeechRecognitionStateChanged(
SpeechRecognizerStatus new_state) {
if (new_state == SPEECH_RECOGNIZER_ERROR) {
speech_recognizer_.reset();
recognizer_status_ = SPEECH_RECOGNIZER_OFF;
controller_->OnTranscriptionError();
} else if (new_state == SPEECH_RECOGNIZER_READY) {
if (recognizer_status_ == SPEECH_RECOGNIZER_OFF && speech_recognizer_) {
// The SpeechRecognizer was initialized after being created, and
// is ready to start recognizing speech.
speech_recognizer_->Start();
}
}
recognizer_status_ = new_state;
}
void ProjectorClientImpl::OnSpeechRecognitionStopped() {
SpeechRecognitionEnded(/*forced=*/false);
}
void ProjectorClientImpl::OnLanguageIdentificationEvent(
media::mojom::LanguageIdentificationEventPtr event) {
// For now, this is ignored by projector.
}
void ProjectorClientImpl::OnFileSystemMounted() {
OnNewScreencastPreconditionChanged(
controller_->GetNewScreencastPrecondition());
}
void ProjectorClientImpl::OnFileSystemBeingUnmounted() {
OnNewScreencastPreconditionChanged(
controller_->GetNewScreencastPrecondition());
}
void ProjectorClientImpl::OnFileSystemMountFailed() {
OnNewScreencastPreconditionChanged(
controller_->GetNewScreencastPrecondition());
}
void ProjectorClientImpl::OnUserSessionStarted(bool is_primary_user) {
if (!is_primary_user || !pref_change_registrar_.IsEmpty()) {
return;
}
Profile* profile = ProfileManager::GetActiveUserProfile();
pref_change_registrar_.Init(profile->GetPrefs());
// TOOD(b/232043809): Consider using the disabled system feature policy
// instead.
pref_change_registrar_.Add(
ash::prefs::kProjectorAllowByPolicy,
base::BindRepeating(&ProjectorClientImpl::OnEnablementPolicyChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
ash::prefs::kProjectorDogfoodForFamilyLinkEnabled,
base::BindRepeating(&ProjectorClientImpl::OnEnablementPolicyChanged,
base::Unretained(this)));
}
void ProjectorClientImpl::MaybeSwitchDriveIntegrationServiceObservation() {
if (drive::DriveIntegrationService* const service =
ProjectorDriveFsProvider::GetActiveDriveIntegrationService()) {
Observe(service);
}
}
void ProjectorClientImpl::SpeechRecognitionEnded(bool forced) {
speech_recognizer_.reset();
recognizer_status_ = SPEECH_RECOGNIZER_OFF;
controller_->OnSpeechRecognitionStopped(forced);
}
void ProjectorClientImpl::OnEnablementPolicyChanged() {
Profile* profile = ProfileManager::GetActiveUserProfile();
ash::SystemWebAppManager* swa_manager =
ash::SystemWebAppManager::Get(profile);
CHECK(swa_manager);
const bool is_installed =
swa_manager->IsSystemWebApp(ash::kChromeUIUntrustedProjectorSwaAppId);
// We can't enable or disable the app if it's not already installed.
if (!is_installed) {
return;
}
const bool is_enabled = IsProjectorAppEnabled(profile);
// The policy has changed to disallow the Projector app. Since we can't
// uninstall the Projector SWA until the user signs out and back in, we should
// close and disable the app for this current session.
if (!is_enabled) {
CloseProjectorApp();
}
auto* web_app_provider = ash::SystemWebAppManager::GetWebAppProvider(profile);
CHECK(web_app_provider);
web_app_provider->on_registry_ready().Post(
FROM_HERE, base::BindOnce(&ProjectorClientImpl::SetAppIsDisabled,
weak_ptr_factory_.GetWeakPtr(), !is_enabled));
}
void ProjectorClientImpl::SetAppIsDisabled(bool disabled) {
Profile* profile = ProfileManager::GetActiveUserProfile();
auto* web_app_provider = ash::SystemWebAppManager::GetWebAppProvider(profile);
CHECK(web_app_provider);
web_app_provider->scheduler().SetAppIsDisabled(
ash::kChromeUIUntrustedProjectorSwaAppId, disabled, base::DoNothing());
}