chromium/chromeos/ash/services/assistant/media_host_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/services/assistant/media_host.h"

#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/services/assistant/media_session/assistant_media_session.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_service.h"
#include "chromeos/ash/services/assistant/test_support/libassistant_media_controller_mock.h"
#include "chromeos/ash/services/assistant/test_support/mock_assistant_interaction_subscriber.h"
#include "chromeos/ash/services/assistant/test_support/scoped_assistant_browser_delegate.h"
#include "chromeos/ash/services/libassistant/public/mojom/android_app_info.mojom-shared.h"
#include "chromeos/ash/services/libassistant/public/mojom/android_app_info.mojom.h"
#include "chromeos/services/assistant/public/shared/utils.h"
#include "services/media_session/public/cpp/test/mock_media_session.h"
#include "services/media_session/public/cpp/test/test_media_controller.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace ash::assistant {

namespace {

using libassistant::mojom::MediaState;
using libassistant::mojom::MediaStatePtr;
using libassistant::mojom::PlaybackState;
using media_session::mojom::MediaSessionInfo;
using media_session::test::MockMediaSessionMojoObserver;
using media_session::test::TestMediaController;
using ::testing::_;

constexpr char kPlayAndroidMediaAction[] = "android.intent.action.VIEW";

#define EXPECT_NO_CALLS(args...) EXPECT_CALL(args).Times(0);

MATCHER_P(PlaybackStateIs, expected_state, "") {
  if (arg.is_null()) {
    *result_listener << "MediaStatePtr is nullptr";
    return false;
  }

  if (arg->playback_state != expected_state) {
    *result_listener << "Expected " << expected_state << " but got "
                     << arg->playback_state;
    return false;
  }
  return true;
}

std::string AndroidAppInfoToString(const AndroidAppInfo& app_info) {
  return base::StringPrintf(R"(
          AndroidAppInfo {
              package_name '%s'
              version '%i'
              localized_app_name '%s'
              action '%s'
              intent '%s'
              status '%i'
          )",
                            app_info.package_name.c_str(), app_info.version,
                            app_info.localized_app_name.c_str(),
                            app_info.action.c_str(), app_info.intent.c_str(),
                            static_cast<int>(app_info.status));
}

MATCHER_P(MatchesAndroidAppInfo, expected, "") {
  if (AndroidAppInfoToString(arg) == AndroidAppInfoToString(expected))
    return true;

  *result_listener << "\nExpected: " << AndroidAppInfoToString(expected);
  *result_listener << "\nActual: " << AndroidAppInfoToString(arg);
  return false;
}

class FakeMediaControllerManager
    : public media_session::mojom::MediaControllerManager {
 public:
  FakeMediaControllerManager() {
    media_controller_ = std::make_unique<TestMediaController>();
  }
  FakeMediaControllerManager(const FakeMediaControllerManager&) = delete;
  FakeMediaControllerManager& operator=(const FakeMediaControllerManager&) =
      delete;
  ~FakeMediaControllerManager() override = default;

  mojo::Receiver<media_session::mojom::MediaControllerManager>& receiver() {
    return receiver_;
  }

  TestMediaController* media_controller() const {
    return media_controller_.get();
  }

  // media_session::mojom::MediaControllerManager implementation:
  void CreateMediaControllerForSession(
      mojo::PendingReceiver<media_session::mojom::MediaController> receiver,
      const ::base::UnguessableToken& request_id) override {
    NOTIMPLEMENTED();
  }
  void CreateActiveMediaController(
      mojo::PendingReceiver<media_session::mojom::MediaController> receiver)
      override {
    media_controller_->BindMediaControllerReceiver(std::move(receiver));
  }
  void SuspendAllSessions() override { NOTIMPLEMENTED(); }

 private:
  mojo::Receiver<media_session::mojom::MediaControllerManager> receiver_{this};
  std::unique_ptr<TestMediaController> media_controller_;
};

}  // namespace

class MediaHostTest : public testing::Test {
 public:
  MediaHostTest() = default;
  MediaHostTest(const MediaHostTest&) = delete;
  MediaHostTest& operator=(const MediaHostTest&) = delete;
  ~MediaHostTest() override = default;

  void SetUp() override {
    delegate_.SetMediaControllerManager(&media_controller_manager_.receiver());

    media_host_ = std::make_unique<MediaHost>(AssistantBrowserDelegate::Get(),
                                              &interaction_subscribers_);
    media_host().Initialize(
        &libassistant_controller_,
        libassistant_media_delegate_.BindNewPipeAndPassReceiver());
  }

  LibassistantMediaControllerMock& libassistant_controller_mock() {
    return libassistant_controller_;
  }

  libassistant::mojom::MediaDelegate& libassistant_media_delegate() {
    return *libassistant_media_delegate_;
  }

  MediaHost& media_host() { return *media_host_; }

  AssistantMediaSession& media_session() {
    return media_host().media_session();
  }

  TestMediaController* media_controller() const {
    return media_controller_manager_.media_controller();
  }

  void FlushMojomPipes() {
    media_controller_manager_.receiver().FlushForTesting();
    libassistant_media_delegate_.FlushForTesting();
  }

  void SetRelatedInfoEnabled(bool enabled) {
    media_host().SetRelatedInfoEnabled(enabled);
    FlushMojomPipes();
  }

  void StartMediaSession(
      base::UnguessableToken token = base::UnguessableToken::Create()) {
    media_controller()->SimulateMediaSessionChanged(token);
    media_controller()->Flush();
  }

  void MediaSessionInfoChanged(
      media_session::mojom::MediaSessionInfoPtr session_info) {
    media_controller()->SimulateMediaSessionInfoChanged(
        std::move(session_info));
    media_controller()->Flush();
  }

  void MediaSessionMetadataChanged(
      const media_session::MediaMetadata& meta_data) {
    media_controller()->SimulateMediaSessionMetadataChanged(meta_data);
    media_controller()->Flush();
  }

  void AddAssistantInteractionSubscriber(
      AssistantInteractionSubscriber* subscriber) {
    interaction_subscribers_.AddObserver(subscriber);
  }

  void ClearAssistantInteractionSubscribers() {
    interaction_subscribers_.Clear();
  }

 private:
  base::test::SingleThreadTaskEnvironment environment_;

  base::ObserverList<AssistantInteractionSubscriber> interaction_subscribers_;
  FakeMediaControllerManager media_controller_manager_;
  ScopedAssistantBrowserDelegate delegate_;
  testing::StrictMock<LibassistantMediaControllerMock> libassistant_controller_;
  mojo::Remote<libassistant::mojom::MediaDelegate> libassistant_media_delegate_;
  std::unique_ptr<MediaHost> media_host_;
};

TEST_F(MediaHostTest, ShouldSupportResumePlaying) {
  EXPECT_CALL(libassistant_controller_mock(), ResumeInternalMediaPlayer);

  media_host().ResumeInternalMediaPlayer();
}

TEST_F(MediaHostTest, ShouldSupportPausePlaying) {
  EXPECT_CALL(libassistant_controller_mock(), PauseInternalMediaPlayer);

  media_host().PauseInternalMediaPlayer();
}

TEST_F(MediaHostTest, ShouldInitiallyNotObserveMediaChanges) {
  EXPECT_EQ(0, media_controller()->add_observer_count());
}

TEST_F(MediaHostTest,
       ShouldStartObservingMediaChangesWhenRelatedInfoIsEnabled) {
  SetRelatedInfoEnabled(true);
  EXPECT_EQ(1, media_controller()->add_observer_count());
  EXPECT_EQ(1, media_controller()->GetActiveObserverCount());
}

TEST_F(MediaHostTest,
       ShouldStopObservingMediaChangesAndFlushStateWhenRelatedInfoIsDisabled) {
  SetRelatedInfoEnabled(true);

  EXPECT_CALL(libassistant_controller_mock(), SetExternalPlaybackState);
  SetRelatedInfoEnabled(false);

  // Note the observer is not unbound, but the connection is severed.
  EXPECT_EQ(1, media_controller()->add_observer_count());
  EXPECT_EQ(0, media_controller()->GetActiveObserverCount());
}

TEST_F(MediaHostTest, ShouldSetTitleWhenCallingSetExternalPlaybackState) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  MediaStatePtr actual_state;
  EXPECT_CALL(libassistant_controller_mock(), SetExternalPlaybackState)
      .WillOnce([&](MediaStatePtr state) { actual_state = std::move(state); });

  media_session::MediaMetadata meta_data;
  meta_data.title = u"the title";
  MediaSessionMetadataChanged(meta_data);

  ASSERT_FALSE(actual_state.is_null());
  ASSERT_FALSE(actual_state->metadata.is_null());
  EXPECT_EQ(actual_state->metadata->title, "the title");
}

TEST_F(MediaHostTest, ShouldDropSensitiveSessions) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  auto session_info = MediaSessionInfo::New();
  session_info->is_sensitive = true;
  MediaSessionInfoChanged(std::move(session_info));

  EXPECT_NO_CALLS(libassistant_controller_mock(), SetExternalPlaybackState);

  media_session::MediaMetadata meta_data;
  MediaSessionMetadataChanged(meta_data);
}

TEST_F(MediaHostTest, ShouldDropInvalidStates) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  auto session_info = MediaSessionInfo::New();
  session_info->state = MediaSessionInfo::SessionState::kSuspended;
  session_info->playback_state =
      media_session::mojom::MediaPlaybackState::kPlaying;
  MediaSessionInfoChanged(std::move(session_info));

  EXPECT_NO_CALLS(libassistant_controller_mock(), SetExternalPlaybackState);

  media_session::MediaMetadata meta_data;
  MediaSessionMetadataChanged(meta_data);
}

TEST_F(MediaHostTest, ShouldSetPlaybackState) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  // State is idle if MediaSessionInfo is never set.
  {
    EXPECT_CALL(
        libassistant_controller_mock(),
        SetExternalPlaybackState(PlaybackStateIs(PlaybackState::kIdle)));
    media_session::MediaMetadata meta_data;
    MediaSessionMetadataChanged(meta_data);
  }

  // State kPlaying.
  {
    EXPECT_CALL(
        libassistant_controller_mock(),
        SetExternalPlaybackState(PlaybackStateIs(PlaybackState::kPlaying)));

    auto session_info = MediaSessionInfo::New();
    session_info->playback_state =
        media_session::mojom::MediaPlaybackState::kPlaying;
    MediaSessionInfoChanged(std::move(session_info));
  }

  // State kPaused.
  {
    EXPECT_CALL(
        libassistant_controller_mock(),
        SetExternalPlaybackState(PlaybackStateIs(PlaybackState::kPaused)));

    auto session_info = MediaSessionInfo::New();
    session_info->playback_state =
        media_session::mojom::MediaPlaybackState::kPaused;
    MediaSessionInfoChanged(std::move(session_info));
  }

  // State is kInvalid if session is inactive
  {
    EXPECT_CALL(
        libassistant_controller_mock(),
        SetExternalPlaybackState(PlaybackStateIs(PlaybackState::kIdle)));

    auto session_info = MediaSessionInfo::New();
    session_info->state = MediaSessionInfo::SessionState::kInactive;
    session_info->playback_state =
        media_session::mojom::MediaPlaybackState::kPaused;
    MediaSessionInfoChanged(std::move(session_info));
  }
}

TEST_F(MediaHostTest, ShouldResetPlaybackStateWhenDisablingRelatedInfo) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  MediaStatePtr actual_state;
  EXPECT_CALL(libassistant_controller_mock(), SetExternalPlaybackState)
      .WillOnce([&](MediaStatePtr state) { actual_state = std::move(state); });

  SetRelatedInfoEnabled(false);

  const MediaStatePtr empty_state = MediaState::New();
  EXPECT_EQ(actual_state, empty_state);
}

TEST_F(MediaHostTest,
       ShouldIgnorePlaybackStateUpdatesWhenRelatedInfoIsDisabled) {
  SetRelatedInfoEnabled(true);
  StartMediaSession();

  // Playback state is updated when disabling related info.
  EXPECT_CALL(libassistant_controller_mock(), SetExternalPlaybackState);
  SetRelatedInfoEnabled(false);

  // But not for any consecutive changes.
  EXPECT_NO_CALLS(libassistant_controller_mock(), SetExternalPlaybackState);
  media_session::MediaMetadata meta_data;
  MediaSessionMetadataChanged(meta_data);
}

TEST_F(MediaHostTest,
       ShouldIgnorePlaybackStateUpdatesForLibassistantInternalSessions) {
  SetRelatedInfoEnabled(true);

  const auto session_id = base::UnguessableToken::Create();
  StartMediaSession(session_id);
  media_session().SetInternalAudioFocusIdForTesting(session_id);

  EXPECT_NO_CALLS(libassistant_controller_mock(), SetExternalPlaybackState);

  media_session::MediaMetadata meta_data;
  MediaSessionMetadataChanged(meta_data);
}

TEST_F(MediaHostTest, ShouldForwardLibassistantMediaSessionUpdates) {
  MockMediaSessionMojoObserver media_session_observer(media_session());
  media_session_observer.WaitForEmptyMetadata();

  auto input = MediaState::New();
  input->metadata = libassistant::mojom::MediaMetadata::New();
  input->metadata->title = "the title";
  input->metadata->artist = "the artist";
  input->metadata->album = "the album";
  libassistant_media_delegate().OnPlaybackStateChanged(std::move(input));
  FlushMojomPipes();

  media_session::MediaMetadata expected_output;
  expected_output.title = u"the title";
  expected_output.artist = u"the artist";
  expected_output.album = u"the album";
  media_session_observer.WaitForExpectedMetadata(expected_output);
}

TEST_F(MediaHostTest, ShouldForwardLibassistantOpenAndroidMediaUpdates) {
  testing::StrictMock<MockAssistantInteractionSubscriber> mock;
  AddAssistantInteractionSubscriber(&mock);

  AndroidAppInfo output_app_info;
  output_app_info.package_name = "the package name";
  output_app_info.version = 111;
  output_app_info.localized_app_name = "the localized name";
  output_app_info.action = kPlayAndroidMediaAction;
  output_app_info.intent = "the intent";
  output_app_info.status = AppStatus::kUnknown;

  EXPECT_CALL(mock, OnOpenAppResponse(MatchesAndroidAppInfo(output_app_info)));

  AndroidAppInfo input_app_info;
  input_app_info.package_name = "the package name";
  input_app_info.version = 111;
  input_app_info.localized_app_name = "the localized name";
  input_app_info.intent = "the intent";
  input_app_info.status = AppStatus::kUnknown;
  input_app_info.action = kPlayAndroidMediaAction;

  libassistant_media_delegate().PlayAndroidMedia(std::move(input_app_info));

  FlushMojomPipes();
  ClearAssistantInteractionSubscribers();
}

TEST_F(MediaHostTest, ShouldOpenWebMediaUrl) {
  testing::StrictMock<MockAssistantInteractionSubscriber> mock;
  AddAssistantInteractionSubscriber(&mock);

  EXPECT_CALL(
      mock, OnOpenUrlResponse(GURL("http://the.url"), /*in_background=*/false));

  libassistant_media_delegate().PlayWebMedia("http://the.url");

  FlushMojomPipes();
  ClearAssistantInteractionSubscribers();
}

TEST_F(MediaHostTest, ShouldPlayNextTrack) {
  EXPECT_EQ(0, media_controller()->next_track_count());
  libassistant_media_delegate().NextTrack();
  FlushMojomPipes();
  EXPECT_EQ(1, media_controller()->next_track_count());
}

TEST_F(MediaHostTest, ShouldPlayPreviousTrack) {
  EXPECT_EQ(0, media_controller()->previous_track_count());
  libassistant_media_delegate().PreviousTrack();
  FlushMojomPipes();
  EXPECT_EQ(1, media_controller()->previous_track_count());
}

TEST_F(MediaHostTest, ShouldPause) {
  EXPECT_EQ(0, media_controller()->suspend_count());
  libassistant_media_delegate().Pause();
  FlushMojomPipes();
  EXPECT_EQ(1, media_controller()->suspend_count());
}

TEST_F(MediaHostTest, ShouldResume) {
  EXPECT_EQ(0, media_controller()->resume_count());
  libassistant_media_delegate().Resume();
  FlushMojomPipes();
  EXPECT_EQ(1, media_controller()->resume_count());
}

TEST_F(MediaHostTest, ShouldStop) {
  EXPECT_EQ(0, media_controller()->suspend_count());
  libassistant_media_delegate().Stop();
  FlushMojomPipes();
  EXPECT_EQ(1, media_controller()->suspend_count());
}

}  // namespace ash::assistant