chromium/chrome/browser/ui/ash/media_client/media_client_impl_unittest.cc

// Copyright 2019 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/media_client/media_client_impl.h"

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "ash/public/cpp/media_controller.h"
#include "ash/public/cpp/test/test_new_window_delegate.h"
#include "chrome/browser/ash/extensions/media_player_api.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/system_notification_helper.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/user_manager/fake_user_manager.h"
#include "media/capture/video/video_capture_device_info.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/accelerators/media_keys_listener.h"

// Gmock matchers and actions that are used below.
using ::testing::AnyOf;

namespace {

class TestMediaController : public ash::MediaController {
 public:
  TestMediaController() = default;

  TestMediaController(const TestMediaController&) = delete;
  TestMediaController& operator=(const TestMediaController&) = delete;

  ~TestMediaController() override = default;

  // ash::MediaController:
  void SetClient(ash::MediaClient* client) override {}
  void SetForceMediaClientKeyHandling(bool enabled) override {
    force_media_client_key_handling_ = enabled;
  }
  void NotifyCaptureState(
      const base::flat_map<AccountId, ash::MediaCaptureState>& capture_states)
      override {}

  void NotifyVmMediaNotificationState(bool camera,
                                      bool mic,
                                      bool camera_and_mic) override {}

  bool force_media_client_key_handling() const {
    return force_media_client_key_handling_;
  }

 private:
  bool force_media_client_key_handling_ = false;
};

class TestMediaKeysDelegate : public ui::MediaKeysListener::Delegate {
 public:
  TestMediaKeysDelegate() = default;

  TestMediaKeysDelegate(const TestMediaKeysDelegate&) = delete;
  TestMediaKeysDelegate& operator=(const TestMediaKeysDelegate&) = delete;

  ~TestMediaKeysDelegate() override = default;

  void OnMediaKeysAccelerator(const ui::Accelerator& accelerator) override {
    last_media_key_ = accelerator;
  }

  std::optional<ui::Accelerator> ConsumeLastMediaKey() {
    std::optional<ui::Accelerator> key = last_media_key_;
    last_media_key_.reset();
    return key;
  }

 private:
  std::optional<ui::Accelerator> last_media_key_;
};

class FakeNotificationDisplayService : public NotificationDisplayService {
 public:
  void Display(
      NotificationHandler::Type notification_type,
      const message_center::Notification& notification,
      std::unique_ptr<NotificationCommon::Metadata> metadata) override {
    show_called_times_++;
    active_notifications_.insert_or_assign(notification.id(), notification);
  }

  void Close(NotificationHandler::Type notification_type,
             const std::string& notification_id) override {
    active_notifications_.erase(notification_id);
  }

  void GetDisplayed(DisplayedNotificationsCallback callback) override {}
  void GetDisplayedForOrigin(const GURL& origin,
                             DisplayedNotificationsCallback callback) override {
  }

  void AddObserver(NotificationDisplayService::Observer* observer) override {}
  void RemoveObserver(NotificationDisplayService::Observer* observer) override {
  }

  // Returns true if any existing notification contains `keywords` as a
  // substring.
  bool HasNotificationMessageContaining(const std::string& keywords) const {
    const std::u16string keywords_u16 = base::UTF8ToUTF16(keywords);
    for (const auto& [notification_id, notification] : active_notifications_) {
      if (notification.message().find(keywords_u16) != std::u16string::npos) {
        return true;
      }
    }
    return false;
  }

  size_t NumberOfActiveNotifications() const {
    return active_notifications_.size();
  }

  size_t show_called_times() const { return show_called_times_; }

  void SimulateClick(const std::string& id, std::optional<int> button_idx) {
    auto notification_iter = active_notifications_.find(id);
    ASSERT_TRUE(notification_iter != active_notifications_.end());

    message_center::Notification notification = notification_iter->second;

    notification.delegate()->Click(button_idx, std::nullopt);

    if (notification.rich_notification_data().remove_on_click) {
      active_notifications_.erase(id);
    }
  }

 private:
  std::map<std::string, message_center::Notification> active_notifications_;
  size_t show_called_times_ = 0;
};

class MockNewWindowDelegate
    : public testing::NiceMock<ash::TestNewWindowDelegate> {
 public:
  // TestNewWindowDelegate:
  MOCK_METHOD(void,
              OpenUrl,
              (const GURL& url, OpenUrlFrom from, Disposition disposition),
              (override));
};

}  // namespace

class MediaClientTest : public BrowserWithTestWindowTest {
 public:
  MediaClientTest() = default;

  MediaClientTest(const MediaClientTest&) = delete;
  MediaClientTest& operator=(const MediaClientTest&) = delete;

  ~MediaClientTest() override = default;

  void SetUp() override {
    BrowserWithTestWindowTest::SetUp();

    alt_window_ = CreateBrowserWindow();
    alt_browser_ = CreateBrowser(alt_profile(), Browser::TYPE_NORMAL, false,
                                 alt_window_.get());

    extensions::MediaPlayerAPI::Get(profile());

    test_delegate_ = std::make_unique<TestMediaKeysDelegate>();

    media_controller_resetter_ =
        std::make_unique<ash::MediaController::ScopedResetterForTest>();
    test_media_controller_ = std::make_unique<TestMediaController>();

    media_client_ = std::make_unique<MediaClientImpl>();
    media_client_->InitForTesting(test_media_controller_.get());

    BrowserList::SetLastActive(browser());

    ASSERT_FALSE(test_media_controller_->force_media_client_key_handling());
    ASSERT_EQ(std::nullopt, delegate()->ConsumeLastMediaKey());
  }

  void TearDown() override {
    media_client_.reset();
    test_media_controller_.reset();
    media_controller_resetter_.reset();
    test_delegate_.reset();

    alt_browser_->tab_strip_model()->CloseAllTabs();
    alt_browser_.reset();
    alt_window_.reset();

    BrowserWithTestWindowTest::TearDown();
  }

  MediaClientImpl* client() { return media_client_.get(); }

  TestMediaController* controller() { return test_media_controller_.get(); }

  Profile* alt_profile() {
    return profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  }

  Browser* alt_browser() { return alt_browser_.get(); }

  TestMediaKeysDelegate* delegate() { return test_delegate_.get(); }

 private:
  std::unique_ptr<TestMediaKeysDelegate> test_delegate_;
  std::unique_ptr<ash::MediaController::ScopedResetterForTest>
      media_controller_resetter_;
  std::unique_ptr<TestMediaController> test_media_controller_;
  std::unique_ptr<MediaClientImpl> media_client_;

  std::unique_ptr<Browser> alt_browser_;
  std::unique_ptr<BrowserWindow> alt_window_;
};

class MediaClientAppUsingCameraTest : public testing::Test {
 public:
  MediaClientAppUsingCameraTest() {
    auto delegate = std::make_unique<MockNewWindowDelegate>();
    new_window_delegate_ = delegate.get();
    window_delegate_provider_ =
        std::make_unique<ash::TestNewWindowDelegateProvider>(
            std::move(delegate));
  }

  void LaunchAppUsingCamera(int active_client_count) {
    media_client_.active_camera_client_count_ = active_client_count;
  }

  void SetCameraHWPrivacySwitchState(
      const std::string& device_id,
      cros::mojom::CameraPrivacySwitchState state) {
    media_client_.device_id_to_camera_privacy_switch_state_[device_id] = state;
  }

  // Adds the device with id `device_id` to the map of active devices. To
  // display hardware switch notifications associated to this device, the device
  // needs to be active.
  void MakeDeviceActive(const std::string& device_id) {
    media_client_
        .devices_used_by_client_[cros::mojom::CameraClientType::CHROME] = {
        device_id};
  }

  void OnActiveClientChange(
      cros::mojom::CameraClientType type,
      const base::flat_set<std::string>& active_device_ids,
      int active_client_count) {
    media_client_.devices_used_by_client_.insert_or_assign(type,
                                                           active_device_ids);
    media_client_.active_camera_client_count_ = active_client_count;

    media_client_.OnGetSourceInfosByActiveClientChanged(
        active_device_ids,
        video_capture::mojom::VideoSourceProvider::GetSourceInfosResult::
            kSuccess,
        video_capture_devices_);
  }

  void AttachCamera(const std::string& device_id,
                    const std::string& device_name) {
    media::VideoCaptureDeviceInfo device_info;
    device_info.descriptor.device_id = device_id;
    device_info.descriptor.set_display_name(device_name);
    video_capture_devices_.push_back(device_info);
  }

  // Detaches the most recently attached camera.
  void DetachCamera() { video_capture_devices_.pop_back(); }

  void ShowCameraOffNotification(const std::string& device_id,
                                 const std::string& device_name) {
    media_client_.ShowCameraOffNotification(device_id, device_name);
  }

  FakeNotificationDisplayService* SetSystemNotificationService() const {
    std::unique_ptr<FakeNotificationDisplayService>
        fake_notification_display_service =
            std::make_unique<FakeNotificationDisplayService>();
    FakeNotificationDisplayService* fake_notification_display_service_ptr =
        fake_notification_display_service.get();
    SystemNotificationHelper::GetInstance()->SetSystemServiceForTesting(
        std::move(fake_notification_display_service));

    return fake_notification_display_service_ptr;
  }

 protected:
  // Has to be the first member as others are CHECKing the environment in their
  // constructors.
  content::BrowserTaskEnvironment task_environment_;

  MediaClientImpl media_client_;
  SystemNotificationHelper system_notification_helper_;
  raw_ptr<MockNewWindowDelegate, DanglingUntriaged> new_window_delegate_ =
      nullptr;
  std::unique_ptr<ash::TestNewWindowDelegateProvider> window_delegate_provider_;
  std::vector<media::VideoCaptureDeviceInfo> video_capture_devices_;
};

TEST_F(MediaClientTest, HandleMediaAccelerators) {
  const struct {
    ui::Accelerator accelerator;
    base::RepeatingClosure client_handler;
  } kTestCases[] = {
      {ui::Accelerator(ui::VKEY_MEDIA_PLAY_PAUSE, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaPlayPause,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_MEDIA_PLAY, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaPlay,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_MEDIA_PAUSE, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaPause,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_MEDIA_STOP, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaStop,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_MEDIA_NEXT_TRACK, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaNextTrack,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_MEDIA_PREV_TRACK, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaPrevTrack,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_OEM_103, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaSeekBackward,
                           base::Unretained(client()))},
      {ui::Accelerator(ui::VKEY_OEM_104, ui::EF_NONE),
       base::BindRepeating(&MediaClientImpl::HandleMediaSeekForward,
                           base::Unretained(client()))}};

  for (auto& test : kTestCases) {
    SCOPED_TRACE(::testing::Message()
                 << "accelerator key:" << test.accelerator.key_code());

    // Enable custom media key handling for the current browser. Ensure that
    // the client set the override on the controller.
    client()->EnableCustomMediaKeyHandler(profile(), delegate());
    EXPECT_TRUE(controller()->force_media_client_key_handling());

    // Simulate the media key and check that the delegate received it.
    test.client_handler.Run();
    EXPECT_EQ(test.accelerator, delegate()->ConsumeLastMediaKey());

    // Change the active browser and ensure the override was disabled.
    BrowserList::SetLastActive(alt_browser());
    EXPECT_FALSE(controller()->force_media_client_key_handling());

    // Simulate the media key and check that the delegate did not receive it.
    test.client_handler.Run();
    EXPECT_EQ(std::nullopt, delegate()->ConsumeLastMediaKey());

    // Change the active browser back and ensure the override was enabled.
    BrowserList::SetLastActive(browser());
    EXPECT_TRUE(controller()->force_media_client_key_handling());

    // Simulate the media key and check the delegate received it.
    test.client_handler.Run();
    EXPECT_EQ(test.accelerator, delegate()->ConsumeLastMediaKey());

    // Disable custom media key handling for the current browser and ensure
    // the override was disabled.
    client()->DisableCustomMediaKeyHandler(profile(), delegate());
    EXPECT_FALSE(controller()->force_media_client_key_handling());

    // Simulate the media key and check the delegate did not receive it.
    test.client_handler.Run();
    EXPECT_EQ(std::nullopt, delegate()->ConsumeLastMediaKey());
  }
}

TEST_F(MediaClientAppUsingCameraTest,
       NotificationRemovedWhenSWSwitchChangedToON) {
  const FakeNotificationDisplayService* notification_display_service =
      SetSystemNotificationService();

  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 0u);

  // Launch an app. The notification shouldn't be displayed yet.
  LaunchAppUsingCamera(/*active_client_count=*/1);
  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 0u);

  // Showing the camera notification, e.g. because the hardware privacy switch
  // was toggled.
  SetCameraHWPrivacySwitchState("device_id",
                                cros::mojom::CameraPrivacySwitchState::ON);
  MakeDeviceActive("device_id");
  ShowCameraOffNotification("device_id", "device_name");
  // One notification should be displayed.
  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 1u);

  // Setting the software privacy switch to ON. The existing hardware switch
  // notification should be removed.
  media_client_.OnCameraSWPrivacySwitchStateChanged(
      cros::mojom::CameraPrivacySwitchState::ON);
  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 0u);
}

TEST_F(MediaClientAppUsingCameraTest, LearnMoreButtonInteraction) {
  FakeNotificationDisplayService* notification_display_service =
      SetSystemNotificationService();

  EXPECT_EQ(notification_display_service->show_called_times(), 0u);

  LaunchAppUsingCamera(/*active_client_count=*/1);

  // Showing the camera notification, e.g. because the privacy switch was
  // toggled.
  SetCameraHWPrivacySwitchState("device_id",
                                cros::mojom::CameraPrivacySwitchState::ON);
  MakeDeviceActive("device_id");
  ShowCameraOffNotification("device_id", "device_name");

  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 1u);
  EXPECT_CALL(*new_window_delegate_, OpenUrl).Times(1);

  notification_display_service->SimulateClick(
      "ash.media.camera.activity_with_privacy_switch_on.device_id", 0);

  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 0u);
}

TEST_F(MediaClientAppUsingCameraTest,
       NotificationRemovedWhenCameraDetachedOrInactive) {
  FakeNotificationDisplayService* notification_display_service =
      SetSystemNotificationService();

  // No notification initially.
  EXPECT_EQ(0u, notification_display_service->NumberOfActiveNotifications());

  const std::string camera1 = "camera1";
  const std::string camera1_name = "Fake camera 1";
  const std::string camera2 = "camera2";
  const std::string camera2_name = "Fake camera 2";

  // Attach two cameras to the device. Both of the cameras have HW switch. Turn
  // the HW switch ON for both of the devices.
  AttachCamera(camera1, camera1_name);
  SetCameraHWPrivacySwitchState(camera1,
                                cros::mojom::CameraPrivacySwitchState::ON);
  AttachCamera(camera2, camera2_name);
  SetCameraHWPrivacySwitchState(camera2,
                                cros::mojom::CameraPrivacySwitchState::ON);

  // Still no notification.
  EXPECT_EQ(notification_display_service->NumberOfActiveNotifications(), 0u);

  // `CHROME` client starts accessing camera1. A hardware switch notification
  // for camera1 should be displayed.
  OnActiveClientChange(cros::mojom::CameraClientType::CHROME, {camera1}, 1);
  EXPECT_EQ(1u, notification_display_service->NumberOfActiveNotifications());
  EXPECT_TRUE(notification_display_service->HasNotificationMessageContaining(
      camera1_name));

  // `CHROME` client starts accessing camera2 as well. A hardware switch
  // notification for camera2 should be displayed.
  OnActiveClientChange(cros::mojom::CameraClientType::CHROME,
                       {camera1, camera2}, 1);
  EXPECT_EQ(2u, notification_display_service->NumberOfActiveNotifications());
  EXPECT_TRUE(notification_display_service->HasNotificationMessageContaining(
      camera2_name));

  // `CHROME` client stops accessing camera1. The respective notification should
  // be removed.
  OnActiveClientChange(cros::mojom::CameraClientType::CHROME, {camera2}, 1);
  EXPECT_EQ(1u, notification_display_service->NumberOfActiveNotifications());
  EXPECT_FALSE(notification_display_service->HasNotificationMessageContaining(
      camera1_name));

  // Detach camera2.
  DetachCamera();
  // `CHROME` client stops accessing camera2 as the camera is detached. The
  // respective notification should be removed.
  OnActiveClientChange(cros::mojom::CameraClientType::CHROME, {}, 0);
  EXPECT_EQ(0u, notification_display_service->NumberOfActiveNotifications());
}