chromium/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl_unittest.cc

// Copyright 2020 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/global_media_controls/media_notification_provider_impl.h"
#include <memory>

#include "ash/system/media/media_notification_provider_observer.h"
#include "ash/test_shell_delegate.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/media_ui_ash.h"
#include "chrome/browser/ash/crosapi/test_crosapi_environment.h"
#include "chrome/browser/media/router/discovery/mdns/dns_sd_registry.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/ui/global_media_controls/cast_media_notification_item.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "chrome/test/base/testing_profile.h"
#include "components/global_media_controls/public/constants.h"
#include "components/global_media_controls/public/media_item_manager.h"
#include "components/global_media_controls/public/media_session_item_producer.h"
#include "components/global_media_controls/public/mojom/device_service.mojom.h"
#include "components/global_media_controls/public/test/mock_device_service.h"
#include "components/global_media_controls/public/views/media_item_ui_footer.h"
#include "components/global_media_controls/public/views/media_item_ui_list_view.h"
#include "components/global_media_controls/public/views/media_item_ui_view.h"
#include "components/media_message_center/mock_media_notification_item.h"
#include "components/media_message_center/notification_theme.h"
#include "components/media_router/common/media_route.h"
#include "content/public/test/browser_task_environment.h"
#include "services/media_session/public/cpp/media_session_service.h"
#include "services/media_session/public/mojom/audio_focus.mojom.h"
#include "services/media_session/public/mojom/media_session.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/view.h"

using global_media_controls::mojom::DeviceListClient;
using global_media_controls::mojom::DeviceListHost;
using global_media_controls::mojom::DevicePickerProvider;
using global_media_controls::test::MockDeviceService;
using media_session::mojom::AudioFocusRequestState;
using media_session::mojom::AudioFocusRequestStatePtr;
using media_session::mojom::MediaSessionInfo;
using media_session::mojom::MediaSessionInfoPtr;

namespace ash {

namespace {

class MockCastMediaNotificationItem : public CastMediaNotificationItem {
 public:
  MockCastMediaNotificationItem(
      const media_router::MediaRoute& route,
      global_media_controls::MediaItemManager* item_manager,
      Profile* profile)
      : CastMediaNotificationItem(route, item_manager, nullptr, profile) {}

  MOCK_METHOD(void, StopCasting, ());
};

class MockMediaNotificationProviderObserver
    : public MediaNotificationProviderObserver {
 public:
  MOCK_METHOD(void, OnNotificationListChanged, ());
  MOCK_METHOD(void, OnNotificationListViewSizeChanged, ());
};

class FakeMediaSessionService : public media_session::MediaSessionService {
 public:
  FakeMediaSessionService() = default;
  FakeMediaSessionService(const FakeMediaSessionService&) = delete;
  FakeMediaSessionService& operator=(const FakeMediaSessionService&) = delete;
  ~FakeMediaSessionService() override = default;

  // media_session::MediaSessionService:
  void BindAudioFocusManager(
      mojo::PendingReceiver<media_session::mojom::AudioFocusManager> receiver)
      override {}
  void BindAudioFocusManagerDebug(
      mojo::PendingReceiver<media_session::mojom::AudioFocusManagerDebug>
          receiver) override {}
  void BindMediaControllerManager(
      mojo::PendingReceiver<media_session::mojom::MediaControllerManager>
          receiver) override {}
};

class MediaTestShellDelegate : public TestShellDelegate {
 public:
  MediaTestShellDelegate() = default;
  ~MediaTestShellDelegate() override = default;

  // ShellDelegate:
  media_session::MediaSessionService* GetMediaSessionService() override {
    return &media_session_service_;
  }
  std::unique_ptr<MediaNotificationProvider> CreateMediaNotificationProvider()
      override {
    return std::make_unique<MediaNotificationProviderImpl>(
        GetMediaSessionService());
  }

 private:
  FakeMediaSessionService media_session_service_;
};

class TestMediaNotificationItem
    : public media_message_center::test::MockMediaNotificationItem {
 public:
  std::optional<base::UnguessableToken> GetSourceId() const override {
    return source_id_;
  }

 private:
  const base::UnguessableToken source_id_{base::UnguessableToken::Create()};
};

}  // namespace

class MediaNotificationProviderImplTest : public ChromeAshTestBase {
 protected:
  MediaNotificationProviderImplTest()
      : ChromeAshTestBase(std::make_unique<content::BrowserTaskEnvironment>(
            content::BrowserTaskEnvironment::REAL_IO_THREAD)) {}
  ~MediaNotificationProviderImplTest() override = default;

  void SetUp() override {
    auto shell_delegate = std::make_unique<MediaTestShellDelegate>();
    shell_delegate_ = shell_delegate.get();
    ChromeAshTestBase::SetUp(std::move(shell_delegate));

    crosapi_environment_.SetUp();
    provider_ = static_cast<MediaNotificationProviderImpl*>(
        MediaNotificationProvider::Get());
    provider_->SetColorTheme(media_message_center::NotificationTheme());
    observer_ = std::make_unique<MockMediaNotificationProviderObserver>();
    provider_->AddObserver(observer_.get());
    layout_provider_ = std::make_unique<ChromeLayoutProvider>();
  }

  void TearDown() override {
    observer_.reset();
    crosapi_environment_.TearDown();
    AshTestBase::TearDown();
  }

  void SimulateShowNotification(base::UnguessableToken id) {
    MediaSessionInfoPtr session_info(MediaSessionInfo::New());
    session_info->is_controllable = true;

    AudioFocusRequestStatePtr focus(AudioFocusRequestState::New());
    focus->request_id = id;
    focus->session_info = std::move(session_info);

    provider_->media_session_item_producer_for_testing()->OnFocusGained(
        std::move(focus));
    provider_->media_session_item_producer_for_testing()->ActivateItem(
        id.ToString());
  }

  void SimulateHideNotification(base::UnguessableToken id) {
    provider_->media_session_item_producer_for_testing()->HideItem(
        id.ToString());
  }

  void SimulateRefreshNotification(base::UnguessableToken id) {
    provider_->media_session_item_producer_for_testing()->RefreshItem(
        id.ToString());
  }

  std::unique_ptr<global_media_controls::MediaItemUIListView>
  CreateNotificationListView() {
    auto view = provider_->GetMediaNotificationListView(
        1, /*should_clip_height=*/true,
        global_media_controls::GlobalMediaControlsEntryPoint::kSystemTray,
        /*show_devices_for_item_id=*/"");
    return base::WrapUnique(
        static_cast<global_media_controls::MediaItemUIListView*>(
            view.release()));
  }

  std::unique_ptr<ChromeLayoutProvider> layout_provider_;
  std::unique_ptr<MockMediaNotificationProviderObserver> observer_;
  raw_ptr<MediaNotificationProviderImpl, DanglingUntriaged> provider_ = nullptr;
  raw_ptr<MediaTestShellDelegate, DanglingUntriaged> shell_delegate_ = nullptr;
  crosapi::TestCrosapiEnvironment crosapi_environment_;
};

TEST_F(MediaNotificationProviderImplTest, NotificationListTest) {
  auto id_1 = base::UnguessableToken::Create();
  auto id_2 = base::UnguessableToken::Create();

  EXPECT_CALL(*observer_, OnNotificationListViewSizeChanged).Times(2);
  SimulateShowNotification(id_1);
  SimulateShowNotification(id_2);
  auto notification_list_view = CreateNotificationListView();
  EXPECT_EQ(notification_list_view->items_for_testing().size(), 2u);

  EXPECT_CALL(*observer_, OnNotificationListViewSizeChanged);
  SimulateHideNotification(id_1);
  EXPECT_EQ(notification_list_view->items_for_testing().size(), 1u);

  provider_->OnBubbleClosing();
}

TEST_F(MediaNotificationProviderImplTest, NotifyObserverOnListChangeTest) {
  auto id = base::UnguessableToken::Create();

  // Expecting 2 calls: one when MediaSessionNotificationItem is created in
  // MediaNotificationService::OnFocusgained, one when
  // MediaNotificationService::ShowNotification is called in
  // SimulateShownotification.
  EXPECT_CALL(*observer_, OnNotificationListChanged).Times(2);
  SimulateShowNotification(id);

  EXPECT_CALL(*observer_, OnNotificationListChanged);
  SimulateHideNotification(id);
}

// Regression test for https://crbug.com/1312419. This should not crash on ASan
// builds (or any other build of course).
TEST_F(MediaNotificationProviderImplTest, DontUseDeletedListView) {
  // Simulate a media session item.
  auto id = base::UnguessableToken::Create();
  SimulateShowNotification(id);

  // Create a list view with that item.
  auto notification_list_view = CreateNotificationListView();

  // Delete the list view.
  notification_list_view.reset();

  // Hide the item. This should not call into the deleted view.
  SimulateHideNotification(id);
}

TEST_F(MediaNotificationProviderImplTest, RefreshMediaItem) {
  auto id = base::UnguessableToken::Create();
  SimulateShowNotification(id);
  auto notification_list_view = CreateNotificationListView();

  EXPECT_EQ(notification_list_view->items_for_testing().size(), 1u);

  EXPECT_CALL(*observer_, OnNotificationListViewSizeChanged);
  SimulateRefreshNotification(id);
  EXPECT_EQ(notification_list_view->items_for_testing().size(), 1u);
}

// Tests the `kGlobalMediaControlsCastStartStop` feature.
// TODO(crbug.com/1407071): Merge this test class into
// MediaNotificationProviderImplTest once the feature is enabled by default on
// Chrome OS.
class CastStartStopMediaNotificationProviderImplTest
    : public MediaNotificationProviderImplTest {
 public:
  void SetUp() override {
    // This must be called before MediaNotificationProviderImplTest::SetUp()
    // starts the GPU service thread.
    scoped_feature_list_.InitAndEnableFeature(
        media_router::kGlobalMediaControlsCastStartStop);
    MediaNotificationProviderImplTest::SetUp();

    profile_ = crosapi_environment_.profile_manager()->CreateTestingProfile(
        "Profile", /*is_main_profile=*/true);
    InitProvider();
  }

  void TearDown() override {
    profile_ = nullptr;
    // This is needed for avoiding a DCHECK failure caused by
    // TestNetworkConnectionTracker having an observer when it's destroyed.
    media_router::DnsSdRegistry::GetInstance()->ResetForTest();

    MediaNotificationProviderImplTest::TearDown();
  }

 protected:
  void InitProvider() {
    provider_->set_profile_for_testing(profile_);
    // We must initialize the list view before we can show individual media
    // items.
    list_view_ = provider_->GetMediaNotificationListView(
        1, /*should_clip_height=*/true,
        global_media_controls::GlobalMediaControlsEntryPoint::kSystemTray,
        /*show_devices_for_item_id=*/"");
  }

  raw_ptr<Profile> profile_ = nullptr;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<views::View> list_view_;
};

TEST_F(CastStartStopMediaNotificationProviderImplTest, ShowCastFooterView) {
  MockCastMediaNotificationItem item{
      media_router::MediaRoute{}, provider_->GetMediaItemManager(), profile_};
  auto* media_item_ui_view =
      provider_->ShowMediaItem("item_id", item.GetWeakPtr());
  global_media_controls::MediaItemUIFooter* footer_view =
      static_cast<global_media_controls::MediaItemUIView*>(media_item_ui_view)
          ->footer_view_for_testing();
  ASSERT_TRUE(footer_view);
  EXPECT_TRUE(footer_view && footer_view->GetVisible());

  // Click on the "Stop casting" button.
  EXPECT_CALL(item, StopCasting());
  views::Button* stop_casting_button =
      static_cast<views::Button*>(footer_view->children()[0]);
  views::test::ButtonTestApi(stop_casting_button)
      .NotifyClick(ui::MouseEvent(
          ui::EventType::kMousePressed, gfx::Point(0, 0), gfx::Point(0, 0),
          ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0));
}

TEST_F(CastStartStopMediaNotificationProviderImplTest, ShowDeviceSelectorView) {
  MockDeviceService device_service;
  TestMediaNotificationItem item;
  provider_->set_device_service_for_testing(&device_service);
  auto* media_item_ui_view =
      provider_->ShowMediaItem("item_id", item.GetWeakPtr());
  global_media_controls::MediaItemUIDeviceSelector* selector_view =
      static_cast<global_media_controls::MediaItemUIView*>(media_item_ui_view)
          ->device_selector_view_for_testing();
  EXPECT_TRUE(selector_view);
}

TEST_F(CastStartStopMediaNotificationProviderImplTest,
       SetDevicePickerProvider) {
  provider_->OnPrimaryUserSessionStarted();

  MockDeviceService device_service;
  EXPECT_CALL(device_service, SetDevicePickerProvider);
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->media_ui_ash()
      ->RegisterDeviceService(base::UnguessableToken::Create(),
                              device_service.PassRemote());
  device_service.FlushForTesting();
}

}  // namespace ash