chromium/chrome/browser/ash/video_conference/video_conference_ash_feature_client_browsertest.cc

// 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 <cstdlib>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_common.h"
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.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/chromeos/video_conference/video_conference_manager_client_common.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace ash {

constexpr char kCrostiniVmId[] = "Linux";
constexpr char kPluginVmId[] = "PluginVm";
constexpr char kBorealisId[] = "Borealis";

class VideoConferenceAshfeatureClientTest : public InProcessBrowserTest {
 public:
  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(
        ash::features::kFeatureManagementVideoConference);

    InProcessBrowserTest::SetUp();
  }

  // Update the permission of current `app_id`.
  void UpdateAppPermision(const std::string& app_id,
                          bool has_camera_permission,
                          bool has_microphone_permission) {
    auto* prefs = ProfileManager::GetActiveUserProfile()->GetPrefs();
    if (app_id == kCrostiniVmId) {
      prefs->SetBoolean(crostini::prefs::kCrostiniMicAllowed,
                        has_microphone_permission);
      CHECK(!has_camera_permission)
          << "Camera is not supported for CrostiniVm yet.";
    }
    if (app_id == kBorealisId) {
      prefs->SetBoolean(borealis::prefs::kBorealisMicAllowed,
                        has_microphone_permission);
      CHECK(!has_camera_permission)
          << "Camera is not supported for CrostiniVm yet.";
    }
    if (app_id == kPluginVmId) {
      prefs->SetBoolean(plugin_vm::prefs::kPluginVmCameraAllowed,
                        has_camera_permission);
      prefs->SetBoolean(plugin_vm::prefs::kPluginVmMicAllowed,
                        has_microphone_permission);
    }
  }

  std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> GetMediaApps() {
    std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> media_app_info;

    VideoConferenceAshFeatureClient::Get()->GetMediaApps(
        base::BindLambdaForTesting(
            [&media_app_info](
                std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr>
                    result) { media_app_info = std::move(result); }));

    return media_app_info;
  }

  // Returns current VideoConferenceMediaState in the VideoConferenceManagerAsh
  VideoConferenceMediaState GetMediaStateInVideoConferenceManagerAsh() {
    return crosapi::CrosapiManager::Get()
        ->crosapi_ash()
        ->video_conference_manager_ash()
        ->GetAggregatedState();
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(VideoConferenceAshfeatureClientTest, GetMediaApps) {
  {
    // Notifying Crostini is using Mic.
    VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
        VmCameraMicManager::VmType::kCrostiniVm,
        VmCameraMicManager::DeviceType::kMic, true);

    // GetMediaApps should return kCrostinId.
    auto media_app_info = GetMediaApps();
    ASSERT_EQ(media_app_info.size(), 1u);
    const auto& info0 = media_app_info[0];

    crosapi::mojom::VideoConferenceMediaAppInfoPtr expected_media_app_info =
        crosapi::mojom::VideoConferenceMediaAppInfo::New(
            /*id=*/info0->id,
            /*last_activity_time=*/info0->last_activity_time,
            /*is_capturing_camera=*/false,
            /*is_capturing_microphone=*/true,
            /*is_capturing_screen=*/false,
            /*title=*/base::UTF8ToUTF16(std::string(kCrostiniVmId)),
            /*url=*/std::nullopt,
            /*app_type=*/crosapi::mojom::VideoConferenceAppType::kCrostiniVm);

    EXPECT_TRUE(media_app_info[0].Equals(expected_media_app_info));

    // Stop accessing should remove the app.
    VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
        VmCameraMicManager::VmType::kCrostiniVm,
        VmCameraMicManager::DeviceType::kMic, false);

    EXPECT_TRUE(GetMediaApps().empty());
  }

  {
    // Notifying PluginVm is using Mic and Camera.
    VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
        VmCameraMicManager::VmType::kPluginVm,
        VmCameraMicManager::DeviceType::kMic, true);
    VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
        VmCameraMicManager::VmType::kPluginVm,
        VmCameraMicManager::DeviceType::kCamera, true);

    // GetMediaApps should return PluginVm.
    auto media_app_info = GetMediaApps();
    ASSERT_EQ(media_app_info.size(), 1u);
    const auto& info0 = media_app_info[0];

    crosapi::mojom::VideoConferenceMediaAppInfoPtr expected_media_app_info =
        crosapi::mojom::VideoConferenceMediaAppInfo::New(
            /*id=*/info0->id,
            /*last_activity_time=*/info0->last_activity_time,
            /*is_capturing_camera=*/true,
            /*is_capturing_microphone=*/true,
            /*is_capturing_screen=*/false,
            /*title=*/base::UTF8ToUTF16(std::string(kPluginVmId)),
            /*url=*/std::nullopt,
            /*app_type=*/crosapi::mojom::VideoConferenceAppType::kPluginVm);

    EXPECT_TRUE(media_app_info[0].Equals(expected_media_app_info));
  }
}

IN_PROC_BROWSER_TEST_F(VideoConferenceAshfeatureClientTest,
                       HandleMediaUsageUpdate) {
  // Set initial permissions.
  UpdateAppPermision(kCrostiniVmId, /*has_camera_permission=*/false,
                     /*has_microphone_permission=*/true);
  UpdateAppPermision(kPluginVmId, /*has_camera_permission=*/true,
                     /*has_microphone_permission=*/false);

  // All state should be false initially.
  VideoConferenceMediaState state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_FALSE(state.has_media_app);
  EXPECT_FALSE(state.has_camera_permission);
  EXPECT_FALSE(state.has_microphone_permission);
  EXPECT_FALSE(state.is_capturing_camera);
  EXPECT_FALSE(state.is_capturing_microphone);
  EXPECT_FALSE(state.is_capturing_screen);

  // Notifying Crostini is using Mic.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kCrostiniVm,
      VmCameraMicManager::DeviceType::kMic, true);

  // State should have update for microphone from CrostiniVm.
  state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_TRUE(state.has_media_app);
  EXPECT_TRUE(state.has_microphone_permission);
  EXPECT_TRUE(state.is_capturing_microphone);
  EXPECT_FALSE(state.has_camera_permission);
  EXPECT_FALSE(state.is_capturing_camera);

  // Notifying PluginVm is using Mic.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kPluginVm,
      VmCameraMicManager::DeviceType::kMic, true);

  // Extra permission obtained from PluginVm.
  state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_TRUE(state.has_camera_permission);
  EXPECT_FALSE(state.is_capturing_camera);

  // Notifying PluginVm is using Camera.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kPluginVm,
      VmCameraMicManager::DeviceType::kCamera, true);

  // Expecting camera capturing from PluginVm.
  state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_TRUE(state.has_camera_permission);
  EXPECT_TRUE(state.is_capturing_camera);

  // Notifying Stopping accessing from PluginVm.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kPluginVm,
      VmCameraMicManager::DeviceType::kMic, false);
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kPluginVm,
      VmCameraMicManager::DeviceType::kCamera, false);

  // Camera permission and capturing should be gone.
  state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_FALSE(state.has_camera_permission);
  EXPECT_FALSE(state.is_capturing_camera);

  // Notifying Stopping accessing from Crostini.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kCrostiniVm,
      VmCameraMicManager::DeviceType::kMic, false);

  state = GetMediaStateInVideoConferenceManagerAsh();
  EXPECT_FALSE(state.has_media_app);
  EXPECT_FALSE(state.has_camera_permission);
  EXPECT_FALSE(state.has_microphone_permission);
  EXPECT_FALSE(state.is_capturing_camera);
  EXPECT_FALSE(state.is_capturing_microphone);
  EXPECT_FALSE(state.is_capturing_screen);
}

IN_PROC_BROWSER_TEST_F(VideoConferenceAshfeatureClientTest,
                       HandleDeviceUsedWhileDisabled) {
  // Notify disabling state of camera and microphone from
  // video_conference_manager_ash.
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->SetSystemMediaDeviceStatus(
          crosapi::mojom::VideoConferenceMediaDevice::kCamera,
          /*disabled=*/true);
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->SetSystemMediaDeviceStatus(
          crosapi::mojom::VideoConferenceMediaDevice::kMicrophone,
          /*disabled=*/true);

  FakeVideoConferenceTrayController* fake_try_controller =
      static_cast<FakeVideoConferenceTrayController*>(
          VideoConferenceTrayController::Get());

  // Notifying Crostini is using Mic.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kCrostiniVm,
      VmCameraMicManager::DeviceType::kMic, true);

  // One UsedWhileDisabled call should be sent to
  // FakeVideoConferenceTrayController.
  ASSERT_EQ(fake_try_controller->device_used_while_disabled_records().size(),
            1u);
  EXPECT_THAT(
      fake_try_controller->device_used_while_disabled_records().back(),
      testing::Pair(crosapi::mojom::VideoConferenceMediaDevice::kMicrophone,
                    base::UTF8ToUTF16(std::string(kCrostiniVmId))));

  // Notifying PluginVm is using Camera.
  VideoConferenceAshFeatureClient::Get()->OnVmDeviceUpdated(
      VmCameraMicManager::VmType::kPluginVm,
      VmCameraMicManager::DeviceType::kCamera, true);

  // Another UsedWhileDisabled call should be sent to
  // FakeVideoConferenceTrayController.
  ASSERT_EQ(fake_try_controller->device_used_while_disabled_records().size(),
            2u);
  EXPECT_THAT(fake_try_controller->device_used_while_disabled_records().back(),
              testing::Pair(crosapi::mojom::VideoConferenceMediaDevice::kCamera,
                            base::UTF8ToUTF16(std::string(kPluginVmId))));
}

}  // namespace ash