// Copyright 2023 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/video_conference/video_conference_ash_feature_client.h"
#include "base/containers/contains.h"
#include "base/strings/utf_string_conversions.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/borealis/borealis_prefs.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_pref_names.h"
#include "chrome/browser/ash/video_conference/video_conference_manager_ash.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/prefs/pref_service.h"
namespace ash {
namespace {
VideoConferenceAshFeatureClient* g_client_instance = nullptr;
constexpr char kCrostiniVmId[] = "Linux";
constexpr char kPluginVmId[] = "PluginVm";
constexpr char kBorealisId[] = "Borealis";
// Returns an "Id" as an identifier for the VmType.
std::string ToVideoConferenceAppId(VmCameraMicManager::VmType vm_type) {
switch (vm_type) {
case VmCameraMicManager::VmType::kCrostiniVm:
return kCrostiniVmId;
case VmCameraMicManager::VmType::kPluginVm:
return kPluginVmId;
case VmCameraMicManager::VmType::kBorealis:
return kBorealisId;
}
}
} // namespace
VideoConferenceAshFeatureClient::VideoConferenceAshFeatureClient()
: client_id_(base::UnguessableToken::Create()),
status_(crosapi::mojom::VideoConferenceMediaUsageStatus::New(
/*client_id=*/client_id_,
/*has_media_app=*/false,
/*has_camera_permission=*/false,
/*has_microphone_permission=*/false,
/*is_capturing_camera=*/false,
/*is_capturing_microphone=*/false,
/*is_capturing_screen=*/false)) {
crosapi::CrosapiManager::Get()
->crosapi_ash()
->video_conference_manager_ash()
->RegisterCppClient(this, client_id_);
CHECK(!g_client_instance);
g_client_instance = this;
}
VideoConferenceAshFeatureClient::~VideoConferenceAshFeatureClient() {
// C++ clients are responsible for manually calling |UnregisterClient| on the
// manager when disconnecting.
crosapi::CrosapiManager::Get()
->crosapi_ash()
->video_conference_manager_ash()
->UnregisterClient(client_id_);
g_client_instance = nullptr;
}
void VideoConferenceAshFeatureClient::GetMediaApps(
GetMediaAppsCallback callback) {
std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> apps;
for (const auto& [app_id, app_state] : id_to_app_state_) {
const std::string app_name = GetAppName(app_id);
apps.push_back(crosapi::mojom::VideoConferenceMediaAppInfo::New(
/*id=*/app_state.token,
/*last_activity_time=*/app_state.last_activity_time,
/*is_capturing_camera=*/app_state.is_capturing_camera,
/*is_capturing_microphone=*/app_state.is_capturing_microphone,
/*is_capturing_screen=*/false,
/*title=*/base::UTF8ToUTF16(app_name),
/*url=*/std::nullopt,
/*app_type=*/GetAppType(app_id)));
}
std::move(callback).Run(std::move(apps));
}
void VideoConferenceAshFeatureClient::ReturnToApp(
const base::UnguessableToken& token,
ReturnToAppCallback callback) {
// Currently, for Vms, we treat the whole VM as one app, so it is not clear
// which one to return to.
std::move(callback).Run(true);
}
void VideoConferenceAshFeatureClient::SetSystemMediaDeviceStatus(
crosapi::mojom::VideoConferenceMediaDevice device,
bool disabled,
SetSystemMediaDeviceStatusCallback callback) {
switch (device) {
case crosapi::mojom::VideoConferenceMediaDevice::kCamera:
camera_system_disabled_ = disabled;
std::move(callback).Run(true);
return;
case crosapi::mojom::VideoConferenceMediaDevice::kMicrophone:
microphone_system_disabled_ = disabled;
std::move(callback).Run(true);
return;
case crosapi::mojom::VideoConferenceMediaDevice::kUnusedDefault:
std::move(callback).Run(false);
return;
}
}
void VideoConferenceAshFeatureClient::StopAllScreenShare() {}
void VideoConferenceAshFeatureClient::OnVmDeviceUpdated(
VmCameraMicManager::VmType vm_type,
VmCameraMicManager::DeviceType device_type,
bool is_capturing) {
const AppIdString& app_id = ToVideoConferenceAppId(vm_type);
const bool is_already_tracked = base::Contains(id_to_app_state_, app_id);
// We only want to start tracking a app if it starts to accessing
// microphone/camera.
if (!is_already_tracked && !is_capturing) {
return;
}
AppState& state = GetOrAddAppState(app_id);
const std::string app_name = GetAppName(app_id);
if (device_type == VmCameraMicManager::DeviceType::kCamera) {
state.is_capturing_camera = is_capturing;
}
if (device_type == VmCameraMicManager::DeviceType::kMic) {
state.is_capturing_microphone = is_capturing;
}
MaybeRemoveApp(app_id);
HandleMediaUsageUpdate();
// This will be an AnchoredNudge, which is only visible if the tray is
// visible; so we have to call this after HandleMediaUsageUpdate.
if (device_type == VmCameraMicManager::DeviceType::kCamera && is_capturing &&
camera_system_disabled_) {
crosapi::CrosapiManager::Get()
->crosapi_ash()
->video_conference_manager_ash()
->NotifyDeviceUsedWhileDisabled(
crosapi::mojom::VideoConferenceMediaDevice::kCamera,
base::UTF8ToUTF16(app_name), base::DoNothingAs<void(bool)>());
}
if (device_type == VmCameraMicManager::DeviceType::kMic && is_capturing &&
microphone_system_disabled_) {
crosapi::CrosapiManager::Get()
->crosapi_ash()
->video_conference_manager_ash()
->NotifyDeviceUsedWhileDisabled(
crosapi::mojom::VideoConferenceMediaDevice::kMicrophone,
base::UTF8ToUTF16(app_name), base::DoNothingAs<void(bool)>());
}
}
// static
VideoConferenceAshFeatureClient* VideoConferenceAshFeatureClient::Get() {
return g_client_instance;
}
// For Ash Features, we simply keep the app_id and app_name as the same.
std::string VideoConferenceAshFeatureClient::GetAppName(
const AppIdString& app_id) {
return app_id;
}
// Get current camera/microphone permission of the `app_id`.
VideoConferenceAshFeatureClient::VideoConferencePermissions
VideoConferenceAshFeatureClient::GetAppPermission(const AppIdString& app_id) {
VideoConferencePermissions permissions{false, false};
// Get permission from prefs based in the app_id.
auto* prefs = ProfileManager::GetActiveUserProfile()->GetPrefs();
if (app_id == kCrostiniVmId) {
permissions.has_microphone_permission =
prefs->GetBoolean(crostini::prefs::kCrostiniMicAllowed);
}
if (app_id == kBorealisId) {
permissions.has_microphone_permission =
prefs->GetBoolean(borealis::prefs::kBorealisMicAllowed);
}
if (app_id == kPluginVmId) {
permissions.has_camera_permission =
prefs->GetBoolean(plugin_vm ::prefs::kPluginVmCameraAllowed);
permissions.has_microphone_permission =
prefs->GetBoolean(plugin_vm ::prefs::kPluginVmMicAllowed);
}
return permissions;
}
crosapi::mojom::VideoConferenceAppType
VideoConferenceAshFeatureClient::GetAppType(const AppIdString& app_id) {
if (app_id == kCrostiniVmId) {
return crosapi::mojom::VideoConferenceAppType::kCrostiniVm;
}
if (app_id == kPluginVmId) {
return crosapi::mojom::VideoConferenceAppType::kPluginVm;
}
if (app_id == kBorealisId) {
return crosapi::mojom::VideoConferenceAppType::kBorealis;
}
return crosapi::mojom::VideoConferenceAppType::kAshClientUnknown;
}
VideoConferenceAshFeatureClient::AppState&
VideoConferenceAshFeatureClient::GetOrAddAppState(const std::string& app_id) {
if (!base::Contains(id_to_app_state_, app_id)) {
id_to_app_state_[app_id] = AppState{base::UnguessableToken::Create(),
base::Time::Now(), false, false};
}
return id_to_app_state_[app_id];
}
void VideoConferenceAshFeatureClient::MaybeRemoveApp(
const AppIdString& app_id) {
if (!id_to_app_state_[app_id].is_capturing_microphone &&
!id_to_app_state_[app_id].is_capturing_camera) {
id_to_app_state_.erase(app_id);
}
}
void VideoConferenceAshFeatureClient::HandleMediaUsageUpdate() {
crosapi::mojom::VideoConferenceMediaUsageStatusPtr new_status =
crosapi::mojom::VideoConferenceMediaUsageStatus::New();
new_status->client_id = client_id_;
new_status->has_media_app = !id_to_app_state_.empty();
for (const auto& [app_id, app_state] : id_to_app_state_) {
new_status->is_capturing_camera |= app_state.is_capturing_camera;
new_status->is_capturing_microphone |= app_state.is_capturing_microphone;
VideoConferencePermissions permissions = GetAppPermission(app_id);
new_status->has_camera_permission |= permissions.has_camera_permission;
new_status->has_microphone_permission |=
permissions.has_microphone_permission;
}
// If `status` equals the previously sent status, don't notify manager.
if (new_status.Equals(status_)) {
return;
}
status_ = new_status->Clone();
auto callback = base::BindOnce([](bool success) {
if (!success) {
LOG(ERROR)
<< "VideoConferenceManager::NotifyMediaUsageUpdate did not succeed.";
}
});
crosapi::CrosapiManager::Get()
->crosapi_ash()
->video_conference_manager_ash()
->NotifyMediaUsageUpdate(std::move(new_status), std::move(callback));
}
} // namespace ash