chromium/chrome/browser/ui/ash/birch/birch_lost_media_provider.cc

// Copyright 2024 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/birch/birch_lost_media_provider.h"

#include "ash/birch/birch_item.h"
#include "ash/birch/birch_model.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/focus_mode/focus_mode_controller.h"
#include "ash/system/focus_mode/focus_mode_util.h"
#include "ash/system/focus_mode/sounds/focus_mode_sounds_controller.h"
#include "ash/system/video_conference/video_conference_common.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/unguessable_token.h"
#include "build/branding_buildflags.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-forward.h"
#include "services/media_session/public/cpp/media_session_service.h"
#include "services/media_session/public/mojom/media_session.mojom-shared.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "chrome/grit/preinstalled_web_apps_resources.h"
#endif

namespace ash {
namespace {

// Returns true if focus mode is playing media (e.g. an audio playlist).
bool IsFocusModePlayingMedia() {
  if (!features::IsFocusModeEnabled()) {
    return false;
  }
  const focus_mode_util::SelectedPlaylist& playlist =
      FocusModeController::Get()
          ->focus_mode_sounds_controller()
          ->selected_playlist();
  return !playlist.empty() &&
         playlist.state == focus_mode_util::SoundState::kPlaying;
}

}  // namespace

BirchLostMediaProvider::BirchLostMediaProvider(Profile* profile)
    : profile_(profile) {
  if (!media_controller_remote_.is_bound()) {
    media_session::MediaSessionService* service =
        Shell::Get()->shell_delegate()->GetMediaSessionService();
    if (!service) {
      return;
    }
    // Connect to the MediaControllerManager and create a MediaController that
    // controls the active session so we can observe it.
    mojo::Remote<media_session::mojom::MediaControllerManager>
        controller_manager_remote;
    service->BindMediaControllerManager(
        controller_manager_remote.BindNewPipeAndPassReceiver());
    controller_manager_remote->CreateActiveMediaController(
        media_controller_remote_.BindNewPipeAndPassReceiver());

    media_controller_remote_->AddObserver(
        media_observer_receiver_.BindNewPipeAndPassRemote());
  }
  if (features::IsBirchVideoConferenceSuggestionsEnabled()) {
    video_conference_controller_ = VideoConferenceTrayController::Get();
  }
}

BirchLostMediaProvider::~BirchLostMediaProvider() = default;

void BirchLostMediaProvider::MediaSessionMetadataChanged(
    const std::optional<media_session::MediaMetadata>& metadata) {
  std::optional<media_session::MediaMetadata> pending_metadata =
      metadata.value_or(media_session::MediaMetadata());
  if (pending_metadata.has_value()) {
    media_title_ = pending_metadata->title;
    source_url_ = pending_metadata->source_title;
  }

  NotifyDataProviderChanged();
}

void BirchLostMediaProvider::MediaSessionInfoChanged(
    media_session::mojom::MediaSessionInfoPtr session_info) {
  // Notify data changed on closure.
  base::ScopedClosureRunner scoped_closure(
      base::BindOnce(&BirchLostMediaProvider::NotifyDataProviderChanged,
                     base::Unretained(this)));

  media_session::mojom::MediaSessionInfoPtr media_session_info =
      std::move(session_info);

  if (!media_session_info) {
    secondary_icon_type_ = SecondaryIconType::kNoIcon;
    return;
  }

  is_playing_ = media_session_info->playback_state ==
                media_session::mojom::MediaPlaybackState::kPlaying;

  if (media_session_info->audio_video_states.has_value() &&
      !media_session_info->audio_video_states->empty()) {
    auto& first_state = media_session_info->audio_video_states->at(0);
    switch (first_state) {
      case media_session::mojom::MediaAudioVideoState::kAudioOnly:
        secondary_icon_type_ = SecondaryIconType::kLostMediaAudio;
        return;
      case media_session::mojom::MediaAudioVideoState::kAudioVideo:
      case media_session::mojom::MediaAudioVideoState::kVideoOnly:
        secondary_icon_type_ = SecondaryIconType::kLostMediaVideo;
        return;
      default:
        secondary_icon_type_ = SecondaryIconType::kNoIcon;
    }
  }
}

void BirchLostMediaProvider::RequestBirchDataFetch() {
  if (video_conference_controller_) {
    video_conference_controller_->GetMediaApps(base::BindOnce(
        &BirchLostMediaProvider::OnVideoConferencingDataAvailable,
        weak_factory_.GetWeakPtr()));
    return;
  }

  // If `video_conference_controller_` doesn't exist, then
  // skip setting vc apps and call to set media apps instead.
  SetMediaAppsFromMediaController();
}

void BirchLostMediaProvider::OnVideoConferencingDataAvailable(
    VideoConferenceManagerAsh::MediaApps apps) {
  std::vector<BirchLostMediaItem> items;

  // If video conference apps exist, return the most recently active app data to
  // the birch model.
  if (!apps.empty()) {
    items.emplace_back(
        /*source_url=*/apps[0]->url.value_or(GURL()),
        /*media_title=*/apps[0]->title,
        /*backup_icon=*/std::nullopt,
        /*secondary_icon_type=*/SecondaryIconType::kLostMediaVideoConference,
        /*activation_callback=*/
        base::BindRepeating(&BirchLostMediaProvider::OnItemPressed,
                            weak_factory_.GetWeakPtr(), apps[0]->id));
    Shell::Get()->birch_model()->SetLostMediaItems(std::move(items));
    return;
  }

  // If video conference apps doesn't exist, then call to set media apps.
  SetMediaAppsFromMediaController();
}

void BirchLostMediaProvider::SetMediaAppsFromMediaController() {
  // Returns early if no media controller is bound or if pertinent media app
  // details are missing.
  if (!media_controller_remote_.is_bound() || !is_playing_ ||
      media_title_.empty() || source_url_.empty()) {
    Shell::Get()->birch_model()->SetLostMediaItems({});
    return;
  }

  // If focus mode is playing audio, don't show the lost media suggestion, since
  // there's no tab associated with the focus mode audio.
  if (IsFocusModePlayingMedia()) {
    Shell::Get()->birch_model()->SetLostMediaItems({});
    return;
  }

  std::optional<ui::ImageModel> backup_icon;
  // The YouTube icon is only available in branded builds.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  if (source_url_.starts_with(u"youtube.com")) {
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    backup_icon = ui::ImageModel::FromImageSkia(
        *rb.GetImageSkiaNamed(IDR_PREINSTALLED_WEB_APPS_YOUTUBE_ICON_192_PNG));
  }
#endif

  std::vector<BirchLostMediaItem> items;
  // `source_url_` doesn't contain necessary prefix to make it a valid GURL so
  // we must append a prefix to it.
  items.emplace_back(
      /*source_url=*/GURL(u"https://www." + source_url_),
      /*media_title=*/media_title_,
      /*backup_icon=*/backup_icon,
      /*secondary_icon_type=*/secondary_icon_type_,
      /*activation_callback=*/
      base::BindRepeating(&BirchLostMediaProvider::OnItemPressed,
                          weak_factory_.GetWeakPtr(), std::nullopt));
  Shell::Get()->birch_model()->SetLostMediaItems(std::move(items));
}

void BirchLostMediaProvider::OnItemPressed(
    std::optional<base::UnguessableToken> vc_id) {
  if (vc_id.has_value()) {
    if (video_conference_controller_) {
      video_conference_controller_->ReturnToApp(vc_id.value());
    }
  } else {
    media_controller_remote_->Raise();
  }
}

void BirchLostMediaProvider::MediaSessionActionsChanged(
    const std::vector<media_session::mojom::MediaSessionAction>& actions) {}

void BirchLostMediaProvider::MediaSessionChanged(
    const std::optional<base::UnguessableToken>& request_id) {}

void BirchLostMediaProvider::MediaSessionPositionChanged(
    const std::optional<media_session::MediaPosition>& position) {}

}  // namespace ash