chromium/chromeos/ash/services/libassistant/media_controller_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/libassistant/media_controller.h"

#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/services/libassistant/grpc/utils/media_status_utils.h"
#include "chromeos/ash/services/libassistant/public/mojom/media_controller.mojom.h"
#include "chromeos/ash/services/libassistant/test_support/fake_assistant_client.h"
#include "chromeos/ash/services/libassistant/test_support/libassistant_service_tester.h"
#include "chromeos/assistant/internal/libassistant/shared_headers.h"
#include "chromeos/assistant/internal/proto/shared/proto/v2/delegate/event_handler_interface.pb.h"
#include "chromeos/assistant/internal/test_support/fake_assistant_manager.h"
#include "chromeos/assistant/internal/util_headers.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::libassistant {

namespace {

using LibassistantPlaybackState = assistant_client::MediaStatus::PlaybackState;
using ProtoAndroidAppInfo = chromeos::assistant::shared::AndroidAppInfo;
using assistant::AndroidAppInfo;
using chromeos::assistant::shared::PlayMediaArgs;
using mojom::PlaybackState;

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

std::string MediaStatusToString(
    const assistant_client::MediaStatus& media_status) {
  return base::StringPrintf(R"(
          MediaStatus {
              playback_state '%i'
              metadata.album '%s'
              metadata.artist '%s'
              metadata.title '%s'
          )",
                            static_cast<int>(media_status.playback_state),
                            media_status.metadata.album.c_str(),
                            media_status.metadata.artist.c_str(),
                            media_status.metadata.title.c_str());
}

MATCHER_P(MatchesMediaStatus, expected, "") {
  if (MediaStatusToString(arg) == MediaStatusToString(expected))
    return true;

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

class MediaDelegateMock : public mojom::MediaDelegate {
 public:
  MediaDelegateMock() = default;
  MediaDelegateMock(const MediaDelegateMock&) = delete;
  MediaDelegateMock& operator=(const MediaDelegateMock&) = delete;
  ~MediaDelegateMock() override = default;

  mojo::PendingRemote<mojom::MediaDelegate> BindNewPipeAndPassRemote() {
    return receiver_.BindNewPipeAndPassRemote();
  }

  void FlushForTesting() { receiver_.FlushForTesting(); }

  // mojom::MediaDelegate implementation:
  MOCK_METHOD(void, OnPlaybackStateChanged, (mojom::MediaStatePtr new_state));
  MOCK_METHOD(void, PlayAndroidMedia, (const AndroidAppInfo& app_info));
  MOCK_METHOD(void, PlayWebMedia, (const std::string& url));
  MOCK_METHOD(void, NextTrack, ());
  MOCK_METHOD(void, PreviousTrack, ());
  MOCK_METHOD(void, Pause, ());
  MOCK_METHOD(void, Resume, ());
  MOCK_METHOD(void, Stop, ());

 private:
  mojo::Receiver<mojom::MediaDelegate> receiver_{this};
};

class MediaManagerMock : public assistant_client::MediaManager {
 public:
  MediaManagerMock() = default;
  MediaManagerMock(const MediaManagerMock&) = delete;
  MediaManagerMock& operator=(const MediaManagerMock&) = delete;
  ~MediaManagerMock() override = default;

  // assistant_client::MediaManager implementation:
  void AddListener(Listener* listener) override { listener_ = listener; }
  MOCK_METHOD(void, Next, ());
  MOCK_METHOD(void, Previous, ());
  MOCK_METHOD(void, Resume, ());
  MOCK_METHOD(void, Pause, ());
  MOCK_METHOD(void, PlayPause, ());
  MOCK_METHOD(void, StopAndClearPlaylist, ());
  MOCK_METHOD(void,
              SetExternalPlaybackState,
              (const assistant_client::MediaStatus& new_status));

  Listener& listener() {
    DCHECK(listener_);
    return *listener_;
  }

 private:
  raw_ptr<Listener> listener_ = nullptr;
};

}  // namespace

class AssistantMediaControllerTest : public testing::Test {
 public:
  AssistantMediaControllerTest()
      : media_controller_(std::make_unique<MediaController>()) {
    media_controller_->Bind(client_.BindNewPipeAndPassReceiver(),
                            delegate_.BindNewPipeAndPassRemote());
  }

  void SetUp() override {
    service_tester_.Start();
    service_tester_.assistant_manager().SetMediaManager(&media_manager_);
    media_controller_->OnAssistantClientRunning(&assistant_client());
  }

  MediaManagerMock& libassistant_media_manager() { return media_manager_; }

  mojo::Remote<mojom::MediaController>& client() { return client_; }

  MediaDelegateMock& delegate() { return delegate_; }

  MediaController& media_controller() {
    DCHECK(media_controller_);
    return *media_controller_;
  }

  void SendPlaybackState(const assistant_client::MediaStatus& input) {
    ::assistant::api::OnDeviceStateEventRequest request;
    auto* status = request.mutable_event()
                       ->mutable_on_state_changed()
                       ->mutable_new_state()
                       ->mutable_media_status();
    ConvertMediaStatusToV2FromV1(input, status);
    media_controller().SendGrpcMessageForTesting(request);
  }

  void CallFallbackMediaHandler(const std::string& action,
                                const std::string& action_proto) {
    ::assistant::api::OnMediaActionFallbackEventRequest request;
    auto* media_action =
        request.mutable_event()->mutable_on_media_action_event();
    media_action->set_action_name(action);
    media_action->set_action_args(action_proto);
    media_controller().SendGrpcMessageForTesting(request);
  }

  void FlushMojomPipes() {
    delegate_.FlushForTesting();
    client_.FlushForTesting();
    service_tester_.FlushForTesting();
  }

  void RemoveAssistantManager() {
    media_controller_->OnDestroyingAssistantClient(&assistant_client());
  }

 private:
  AssistantClient& assistant_client() {
    return service_tester_.assistant_client();
  }

  base::test::SingleThreadTaskEnvironment environment_;

  MediaManagerMock media_manager_;
  mojo::Remote<mojom::MediaController> client_;
  testing::StrictMock<MediaDelegateMock> delegate_;
  std::unique_ptr<MediaController> media_controller_;
  LibassistantServiceTester service_tester_;
};

TEST_F(AssistantMediaControllerTest,
       ShouldSendResumeToLibassistantMediaPlayer) {
  EXPECT_CALL(libassistant_media_manager(), Resume);

  media_controller().ResumeInternalMediaPlayer();
}

TEST_F(AssistantMediaControllerTest, ShouldSendPauseToLibassistantMediaPlayer) {
  EXPECT_CALL(libassistant_media_manager(), Pause);

  media_controller().PauseInternalMediaPlayer();
}

TEST_F(AssistantMediaControllerTest, ShouldSendMediaStatusToLibassistant) {
  EXPECT_CALL(libassistant_media_manager(), SetExternalPlaybackState);

  auto input = mojom::MediaState::New();
  media_controller().SetExternalPlaybackState(std::move(input));
}

TEST_F(AssistantMediaControllerTest, ShouldSendMediaMetadataToLibassistant) {
  assistant_client::MediaStatus expected;
  expected.metadata.album = "album";
  expected.metadata.artist = "artist";
  expected.metadata.title = "title";
  EXPECT_CALL(libassistant_media_manager(),
              SetExternalPlaybackState(MatchesMediaStatus(expected)));

  auto input = mojom::MediaState::New();
  input->metadata = mojom::MediaMetadata::New();
  input->metadata->album = "album";
  input->metadata->artist = "artist";
  input->metadata->title = "title";
  media_controller().SetExternalPlaybackState(std::move(input));
}

TEST_F(AssistantMediaControllerTest,
       ShouldSendMediaPlaybackStateToLibassistant) {
  std::vector<std::pair<PlaybackState, LibassistantPlaybackState>> pairs = {
      {PlaybackState::kError, LibassistantPlaybackState::ERROR},
      {PlaybackState::kIdle, LibassistantPlaybackState::IDLE},
      {PlaybackState::kPaused, LibassistantPlaybackState::PAUSED},
      {PlaybackState::kNewTrack, LibassistantPlaybackState::NEW_TRACK},
      {PlaybackState::kPlaying, LibassistantPlaybackState::PLAYING},
  };

  for (auto pair : pairs) {
    assistant_client::MediaStatus expected;
    expected.playback_state = pair.second;
    EXPECT_CALL(libassistant_media_manager(),
                SetExternalPlaybackState(MatchesMediaStatus(expected)));

    auto input = mojom::MediaState::New();
    input->playback_state = pair.first;
    media_controller().SetExternalPlaybackState(std::move(input));

    testing::Mock::VerifyAndClearExpectations(&libassistant_media_manager());
  }
}

TEST_F(AssistantMediaControllerTest,
       ShouldNotCrashIfAssistantManagerIsNotPresent) {
  RemoveAssistantManager();

  media_controller().ResumeInternalMediaPlayer();
  media_controller().PauseInternalMediaPlayer();
  media_controller().SetExternalPlaybackState(mojom::MediaState::New());
}

TEST_F(AssistantMediaControllerTest, ShouldSendPlaybackStateChangeToDelegate) {
  mojom::MediaStatePtr actual;
  EXPECT_CALL(delegate(), OnPlaybackStateChanged)
      .WillOnce([&](mojom::MediaStatePtr state) { actual = std::move(state); });

  assistant_client::MediaStatus input;
  input.metadata.album = "album";
  input.metadata.artist = "artist";
  input.metadata.title = "title";
  SendPlaybackState(input);
  FlushMojomPipes();

  ASSERT_FALSE(actual.is_null());
  ASSERT_FALSE(actual->metadata.is_null());
  EXPECT_EQ(actual->metadata->album, "album");
  EXPECT_EQ(actual->metadata->artist, "artist");
  EXPECT_EQ(actual->metadata->title, "title");
}

TEST_F(AssistantMediaControllerTest, ShouldSendPlaybackStateToDelegate) {
  std::vector<std::pair<PlaybackState, LibassistantPlaybackState>> pairs = {
      {PlaybackState::kError, LibassistantPlaybackState::ERROR},
      {PlaybackState::kIdle, LibassistantPlaybackState::IDLE},
      {PlaybackState::kPaused, LibassistantPlaybackState::PAUSED},
      {PlaybackState::kNewTrack, LibassistantPlaybackState::NEW_TRACK},
      {PlaybackState::kPlaying, LibassistantPlaybackState::PLAYING},
  };

  for (auto pair : pairs) {
    mojom::MediaStatePtr actual;
    EXPECT_CALL(delegate(), OnPlaybackStateChanged)
        .WillOnce(
            [&](mojom::MediaStatePtr state) { actual = std::move(state); });

    assistant_client::MediaStatus input;
    input.playback_state = pair.second;
    SendPlaybackState(input);
    FlushMojomPipes();

    ASSERT_FALSE(actual.is_null());
    EXPECT_EQ(actual->playback_state, pair.first);
  }
}

TEST_F(AssistantMediaControllerTest, ShouldSupportNext) {
  EXPECT_CALL(delegate(), NextTrack);
  CallFallbackMediaHandler("media.NEXT", "");
  FlushMojomPipes();
}

TEST_F(AssistantMediaControllerTest, ShouldSupportPrevious) {
  EXPECT_CALL(delegate(), PreviousTrack);
  CallFallbackMediaHandler("media.PREVIOUS", "");
  FlushMojomPipes();
}

TEST_F(AssistantMediaControllerTest, ShouldSupportPause) {
  EXPECT_CALL(delegate(), Pause);
  CallFallbackMediaHandler("media.PAUSE", "");
  FlushMojomPipes();
}

TEST_F(AssistantMediaControllerTest, ShouldSupportResume) {
  EXPECT_CALL(delegate(), Resume);
  CallFallbackMediaHandler("media.RESUME", "");
  FlushMojomPipes();
}

TEST_F(AssistantMediaControllerTest, ShouldSupportStop) {
  EXPECT_CALL(delegate(), Stop);
  CallFallbackMediaHandler("media.STOP", "");
  FlushMojomPipes();
}

TEST_F(AssistantMediaControllerTest, ShouldSupportPlayAndroidMedia) {
  PlayMediaArgs play_media_args;
  PlayMediaArgs::MediaItem* media_item = play_media_args.add_media_item();
  ProtoAndroidAppInfo* android_app_info =
      media_item->mutable_provider()->mutable_android_app_info();
  android_app_info->set_package_name("package");
  android_app_info->set_localized_app_name("app name");
  android_app_info->set_app_version(111);
  media_item->set_uri("http://the/uri");

  std::optional<AndroidAppInfo> actual;
  EXPECT_CALL(delegate(), PlayAndroidMedia)
      .WillOnce([&](const AndroidAppInfo& a) { actual = a; });

  CallFallbackMediaHandler("media.PLAY_MEDIA",
                           play_media_args.SerializeAsString());
  FlushMojomPipes();

  ASSERT_TRUE(actual.has_value());
  EXPECT_EQ(actual.value().package_name, "package");
  EXPECT_EQ(actual.value().localized_app_name, "app name");
  EXPECT_EQ(actual.value().version, 111);
  EXPECT_EQ(actual.value().action, "android.intent.action.VIEW");
  EXPECT_EQ(actual.value().intent, "http://the/uri");
}

TEST_F(AssistantMediaControllerTest, ShouldSupportPlayWebMedia) {
  PlayMediaArgs play_media_args;
  PlayMediaArgs::MediaItem* media_item = play_media_args.add_media_item();
  media_item->set_uri("http://the/url");

  std::string actual_url = "<not-called>";
  EXPECT_CALL(delegate(), PlayWebMedia).WillOnce([&](std::string url) {
    actual_url = url;
  });

  CallFallbackMediaHandler("media.PLAY_MEDIA",
                           play_media_args.SerializeAsString());
  FlushMojomPipes();

  EXPECT_EQ(actual_url, "http://the/url");
}

TEST_F(AssistantMediaControllerTest, ShouldIgnoreInvalidUrls) {
  PlayMediaArgs play_media_args;
  PlayMediaArgs::MediaItem* media_item = play_media_args.add_media_item();
  media_item->set_uri("not-a-url");

  EXPECT_NO_CALLS(delegate(), PlayWebMedia);

  CallFallbackMediaHandler("media.PLAY_MEDIA",
                           play_media_args.SerializeAsString());
  FlushMojomPipes();
}
}  // namespace ash::libassistant