chromium/chrome/browser/chromeos/video_conference/video_conference_manager_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/chromeos/video_conference/video_conference_manager_client.h"

#include <utility>
#include <vector>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/unguessable_token.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/chromeos/video_conference/video_conference_manager_client_common.h"
#include "chrome/browser/chromeos/video_conference/video_conference_media_listener.h"
#include "chrome/browser/chromeos/video_conference/video_conference_web_app.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"
#include "extensions/browser/process_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest.h"
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/lacros/lacros_service.h"
#else
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/video_conference/video_conference_manager_ash.h"
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

namespace video_conference {
namespace {

// Returns whether the `contents` is a WebApp.
bool IsWebApp(content::WebContents* contents) {
  return web_app::AppBrowserController::IsWebApp(
      chrome::FindBrowserWithTab(contents));
}

// Returns the AppType of the `contents`.
// We only handled cases that are relevant to video conference apps.
crosapi::mojom::VideoConferenceAppType GetAppType(
    content::WebContents* contents) {
  auto* ext = extensions::ProcessManager::Get(contents->GetBrowserContext())
                  ->GetExtensionForWebContents(contents);
  if (ext) {
    auto type = ext->GetType();
    if (type == extensions::Manifest::TYPE_EXTENSION) {
      return crosapi::mojom::VideoConferenceAppType::kChromeExtension;
    }
    if (type == extensions::Manifest::TYPE_PLATFORM_APP) {
      return crosapi::mojom::VideoConferenceAppType::kChromeApp;
    }

    return crosapi::mojom::VideoConferenceAppType::kBrowserUnknown;
  }
  if (IsWebApp(contents)) {
    return crosapi::mojom::VideoConferenceAppType::kWebApp;
  }

  return crosapi::mojom::VideoConferenceAppType::kChromeTab;
}

}  // namespace

VideoConferenceManagerClientImpl::VideoConferenceManagerClientImpl()
    : 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)) {
  media_listener_ = std::make_unique<
      VideoConferenceMediaListener>(/*media_usage_update_callback=*/
                                    base::BindRepeating(
                                        &VideoConferenceManagerClientImpl::
                                            HandleMediaUsageUpdate,
                                        base::Unretained(this)),
                                    /*create_vc_web_app_callback=*/
                                    base::BindRepeating(
                                        &VideoConferenceManagerClientImpl::
                                            CreateVideoConferenceWebApp,
                                        base::Unretained(this)),
                                    /*device_used_while_disabled_callback=*/
                                    base::BindRepeating(
                                        &VideoConferenceManagerClientImpl::
                                            HandleDeviceUsedWhileDisabled,
                                        base::Unretained(this)));

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  // Bind remote and pass receiver to VideoConferenceManagerAsh.
  chromeos::LacrosService::Get()->BindVideoConferenceManager(
      remote_.BindNewPipeAndPassReceiver());
  // Register the mojo client.
  remote_->RegisterMojoClient(receiver_.BindNewPipeAndPassRemote(), client_id_,
                              base::BindOnce([](bool success) {
                                if (!success) {
                                  LOG(ERROR)
                                      << "VideoConferenceManagerClientImpl "
                                         "RegisterMojoClient did not succeed.";
                                }
                              }));
#else
  // Register the C++ (non-mojo) client.
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->RegisterCppClient(this, client_id_);
#endif
}

VideoConferenceManagerClientImpl::~VideoConferenceManagerClientImpl() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
  // C++ clients are responsible for manually calling |UnregisterClient| on the
  // manager when disconnecting.
  if (crosapi::CrosapiManager::IsInitialized()) {
    crosapi::CrosapiManager::Get()
        ->crosapi_ash()
        ->video_conference_manager_ash()
        ->UnregisterClient(client_id_);
  }
#endif
}

void VideoConferenceManagerClientImpl::RemoveMediaApp(
    const base::UnguessableToken& id) {
  DCHECK(base::Contains(id_to_webcontents_, id));
  auto it = id_to_webcontents_.find(id);
  raw_ptr<content::WebContents> web_contents = it->second;

  // If an associated `WebContentsUserData` exists for this `web_contents`,
  // remove it. This is the case on a primary page change. We don't want to
  // persist the old `WebContentsUserData` but rather create a new one if/when
  // the new page begins capturing camera/mic/screen.
  if (content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
          web_contents)) {
    web_contents->RemoveUserData(
        content::WebContentsUserData<VideoConferenceWebApp>::UserDataKey());
  }

  id_to_webcontents_.erase(it);

  // Send a client update notification to the VCManager.
  SendClientUpdate(crosapi::mojom::VideoConferenceClientUpdate::New(
      /*added_or_removed_app=*/crosapi::mojom::VideoConferenceAppUpdate::
          kAppRemoved,
      /*title_change_info=*/nullptr));

  HandleMediaUsageUpdate();
}

VideoConferenceWebApp*
VideoConferenceManagerClientImpl::CreateVideoConferenceWebApp(
    content::WebContents* web_contents) {
  base::UnguessableToken id = base::UnguessableToken::Create();

  // Callback to handle cleanup when the webcontents is destroyed or its primary
  // page changes.
  auto remove_media_app_callback =
      base::BindRepeating(&VideoConferenceManagerClientImpl::RemoveMediaApp,
                          weak_ptr_factory_.GetWeakPtr());

  // Callback for `VideoConferenceWebApp`s to send client updates (currently,
  // only on title changes).
  auto client_update_callback =
      base::BindRepeating(&VideoConferenceManagerClientImpl::SendClientUpdate,
                          weak_ptr_factory_.GetWeakPtr());

  content::WebContentsUserData<VideoConferenceWebApp>::CreateForWebContents(
      web_contents, id, std::move(remove_media_app_callback),
      std::move(client_update_callback));

  id_to_webcontents_.insert({id, web_contents});

  // Send a client update notification to the VCManager.
  SendClientUpdate(crosapi::mojom::VideoConferenceClientUpdate::New(
      /*added_or_removed_app=*/crosapi::mojom::VideoConferenceAppUpdate::
          kAppAdded,
      /*title_change_info=*/nullptr));

  return content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
      web_contents);
}

void VideoConferenceManagerClientImpl::HandleMediaUsageUpdate() {
  bool is_capturing_camera = false;
  bool is_capturing_microphone = false;
  bool is_capturing_screen = false;

  for (auto [id, web_contents] : id_to_webcontents_) {
    auto* web_app =
        content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
            web_contents);
    DCHECK(web_app)
        << "WebContents with no corresponding VideoConferenceWebApp.";

    is_capturing_camera |= web_app->state().is_capturing_camera;
    is_capturing_microphone |= web_app->state().is_capturing_microphone;
    is_capturing_screen |= web_app->state().is_capturing_screen;
  }

  auto permissions = GetAggregatedPermissions();

  crosapi::mojom::VideoConferenceMediaUsageStatusPtr status =
      crosapi::mojom::VideoConferenceMediaUsageStatus::New(
          /*client_id=*/client_id_,
          /*has_media_app=*/!id_to_webcontents_.empty(),
          /*has_camera_permission=*/permissions.has_camera_permission,
          /*has_microphone_permission=*/permissions.has_microphone_permission,
          /*is_capturing_camera=*/is_capturing_camera,
          /*is_capturing_microphone=*/is_capturing_microphone,
          /*is_capturing_screen=*/is_capturing_screen);

  // If `status` equal the previously sent status, don't notify manager.
  if (status.Equals(status_)) {
    return;
  }
  status_ = status->Clone();

  NotifyManager(std::move(status));
}

void VideoConferenceManagerClientImpl::HandleDeviceUsedWhileDisabled(
    crosapi::mojom::VideoConferenceMediaDevice device,
    const std::u16string& app_name) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  remote_->NotifyDeviceUsedWhileDisabled(std::move(device), app_name,
                                         base::DoNothing());
#else
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->NotifyDeviceUsedWhileDisabled(std::move(device), app_name,
                                      base::DoNothing());
#endif
}

void VideoConferenceManagerClientImpl::GetMediaApps(
    GetMediaAppsCallback callback) {
  std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> apps;

  for (auto [_, web_contents] : id_to_webcontents_) {
    auto* web_app =
        content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
            web_contents);
    DCHECK(web_app)
        << "WebContents with no corresponding VideoConferenceWebApp.";

    auto& app_state = web_app->state();

    apps.push_back(crosapi::mojom::VideoConferenceMediaAppInfo::New(
        /*id=*/app_state.id,
        /*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=*/app_state.is_capturing_screen,
        /*title=*/web_contents->GetTitle(),
        /*url=*/web_contents->GetURL(),
        /*app_type=*/GetAppType(web_contents)));
  }

  std::move(callback).Run(std::move(apps));
}

void VideoConferenceManagerClientImpl::ReturnToApp(
    const base::UnguessableToken& id,
    ReturnToAppCallback callback) {
  if (auto it = id_to_webcontents_.find(id); it != id_to_webcontents_.end()) {
    auto* web_app =
        content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
            it->second);
    DCHECK(web_app)
        << "WebContents with no corresponding VideoConferenceWebApp.";

    web_app->ActivateApp();
    std::move(callback).Run(true);
  } else {
    // As the manager calls `ReturnToApp` on all clients, it is normal and
    // expected that a client doesn't have any `VideoConferenceWebApp` with the
    // provided `id`.
    std::move(callback).Run(false);
  }
}

void VideoConferenceManagerClientImpl::SetSystemMediaDeviceStatus(
    crosapi::mojom::VideoConferenceMediaDevice device,
    bool disabled,
    SetSystemMediaDeviceStatusCallback callback) {
  media_listener_->SetSystemMediaDeviceStatus(std::move(device), disabled);
  std::move(callback).Run(true);
}

void VideoConferenceManagerClientImpl::StopAllScreenShare() {
  for (const auto& pair : id_to_webcontents_) {
    auto* web_app =
        content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
            pair.second);
    DCHECK(web_app)
        << "WebContents with no corresponding VideoConferenceWebApp.";
    if (web_app->state().is_capturing_screen) {
      MediaCaptureDevicesDispatcher::GetInstance()
          ->GetMediaStreamCaptureIndicator()
          ->StopMediaCapturing(
              pair.second,
              MediaStreamCaptureIndicator::MediaType::kDisplayMedia);
    }
  }
}

void VideoConferenceManagerClientImpl::NotifyManager(
    crosapi::mojom::VideoConferenceMediaUsageStatusPtr status) {
  auto callback = base::BindOnce([](bool success) {
    if (!success) {
      LOG(ERROR)
          << "VideoConferenceManager::NotifyMediaUsageUpdate did not succeed.";
    }
  });

  // Send updated media usage state to VcManager.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  remote_->NotifyMediaUsageUpdate(std::move(status), std::move(callback));
#else
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->NotifyMediaUsageUpdate(std::move(status), std::move(callback));
#endif
}

VideoConferencePermissions
VideoConferenceManagerClientImpl::GetAggregatedPermissions() {
  bool has_camera_permission = false;
  bool has_microphone_permission = false;

  for (auto& [_, web_contents] : id_to_webcontents_) {
    auto* web_app =
        content::WebContentsUserData<VideoConferenceWebApp>::FromWebContents(
            web_contents);
    DCHECK(web_app)
        << "WebContents with no corresponding VideoConferenceWebApp.";

    auto permissions = web_app->GetPermissions();
    has_camera_permission |= permissions.has_camera_permission;
    has_microphone_permission |= permissions.has_microphone_permission;
  }

  return {.has_camera_permission = has_camera_permission,
          .has_microphone_permission = has_microphone_permission};
}

void VideoConferenceManagerClientImpl::SendClientUpdate(
    crosapi::mojom::VideoConferenceClientUpdatePtr update) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  remote_->NotifyClientUpdate(std::move(update));
#else
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->NotifyClientUpdate(std::move(update));
#endif
}

}  // namespace video_conference