chromium/chrome/browser/ui/ash/cast_config/cast_config_controller_media_router.cc

// Copyright 2015 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/cast_config/cast_config_controller_media_router.h"

#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/url_constants.h"
#include "components/media_router/browser/media_router.h"
#include "components/media_router/browser/media_router_factory.h"
#include "components/media_router/browser/media_routes_observer.h"
#include "components/media_router/browser/media_sinks_observer.h"
#include "components/media_router/common/media_sink.h"
#include "components/media_router/common/media_source.h"
#include "components/user_manager/user_manager.h"
#include "third_party/icu/source/common/unicode/uversion.h"
#include "third_party/icu/source/i18n/unicode/coll.h"

namespace {

std::optional<media_router::MediaRouter*> g_media_router_for_test;

Profile* GetProfile() {
  if (!user_manager::UserManager::IsInitialized())
    return nullptr;

  auto* user = user_manager::UserManager::Get()->GetPrimaryUser();
  if (!user)
    return nullptr;

  return ash::ProfileHelper::Get()->GetProfileByUser(user);
}

// Returns the MediaRouter instance for the current primary profile, if there is
// one.
media_router::MediaRouter* GetMediaRouter() {
  if (g_media_router_for_test) {
    return *g_media_router_for_test;
  }

  Profile* profile = GetProfile();
  if (!profile || !media_router::MediaRouterEnabled(profile))
    return nullptr;

  auto* router =
      media_router::MediaRouterFactory::GetApiForBrowserContext(profile);
  DCHECK(router);
  return router;
}

}  // namespace

// This class caches the values that the observers give us so we can query them
// at any point in time. It also emits a device refresh event when new data is
// available.
class CastDeviceCache : public media_router::MediaRoutesObserver,
                        public media_router::MediaSinksObserver {
 public:
  using MediaSinks = std::vector<media_router::MediaSink>;
  using MediaRoutes = std::vector<media_router::MediaRoute>;
  using MediaRouteIds = std::vector<media_router::MediaRoute::Id>;

  explicit CastDeviceCache(
      const base::RepeatingClosure& update_devices_callback);

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

  ~CastDeviceCache() override;

  // This may run |update_devices_callback_| before returning.
  void Init();

  const MediaSinks& sinks() const { return sinks_; }
  const MediaRoutes& routes() const { return routes_; }

 private:
  // media_router::MediaSinksObserver:
  void OnSinksReceived(const MediaSinks& sinks) override;

  // media_router::MediaRoutesObserver:
  void OnRoutesUpdated(const MediaRoutes& routes) override;

  // Sorts `sinks_` alphabetically.
  void SortSinks();

  MediaSinks sinks_;
  MediaRoutes routes_;

  std::unique_ptr<icu::Collator> collator_;
  base::RepeatingClosure update_devices_callback_;
};

CastDeviceCache::CastDeviceCache(
    const base::RepeatingClosure& update_devices_callback)
    : MediaRoutesObserver(GetMediaRouter()),
      MediaSinksObserver(GetMediaRouter(),
                         media_router::MediaSource::ForUnchosenDesktop(),
                         url::Origin()),
      update_devices_callback_(update_devices_callback) {}

CastDeviceCache::~CastDeviceCache() = default;

void CastDeviceCache::Init() {
  CHECK(MediaSinksObserver::Init());
}

void CastDeviceCache::OnSinksReceived(const MediaSinks& sinks) {
  sinks_.clear();
  for (const media_router::MediaSink& sink : sinks) {
    // The media router adds a MediaSink instance that doesn't have a name. Make
    // sure to filter that sink out from the UI so it is not rendered, as it
    // will be a line that only has a icon with no apparent meaning.
    if (sink.name().empty())
      continue;

    sinks_.push_back(sink);
  }
  SortSinks();
  update_devices_callback_.Run();
}

void CastDeviceCache::OnRoutesUpdated(const MediaRoutes& routes) {
  routes_ = routes;
  update_devices_callback_.Run();
}

void CastDeviceCache::SortSinks() {
  if (sinks_.size() <= 1) {
    return;
  }
  if (!collator_) {
    UErrorCode error = U_ZERO_ERROR;
    const std::string& locale = g_browser_process->GetApplicationLocale();
    collator_.reset(
        icu::Collator::createInstance(icu::Locale(locale.c_str()), error));
    if (U_FAILURE(error)) {
      collator_.reset();
      return;
    }
  }
  const icu::Collator* collator_ptr = collator_.get();
  std::sort(sinks_.begin(), sinks_.end(),
            [collator_ptr](const media_router::MediaSink& sink1,
                           const media_router::MediaSink& sink2) {
              return sink1.CompareUsingCollator(sink2, collator_ptr);
            });
}

////////////////////////////////////////////////////////////////////////////////
// CastConfigControllerMediaRouter:

CastConfigControllerMediaRouter::CastConfigControllerMediaRouter() {
  // TODO(jdufault): This should use a callback interface once there is an
  // equivalent. See crbug.com/666005.
  session_observation_.Observe(session_manager::SessionManager::Get());
}

CastConfigControllerMediaRouter::~CastConfigControllerMediaRouter() {
  StopObservingMirroringMediaControllerHosts();
}

void CastConfigControllerMediaRouter::OnFreezeInfoChanged() {
  UpdateDevices();
}

// static
void CastConfigControllerMediaRouter::SetMediaRouterForTest(
    media_router::MediaRouter* media_router) {
  g_media_router_for_test = media_router;
}

CastDeviceCache* CastConfigControllerMediaRouter::device_cache() {
  // The CastDeviceCache instance is lazily allocated because the MediaRouter
  // component is not ready when the constructor is invoked.
  if (!device_cache_ && GetMediaRouter()) {
    device_cache_ = std::make_unique<CastDeviceCache>(base::BindRepeating(
        &CastConfigControllerMediaRouter::RequestDeviceRefresh,
        base::Unretained(this)));
    device_cache_->Init();
  }

  return device_cache_.get();
}

void CastConfigControllerMediaRouter::AddObserver(
    CastConfigController::Observer* observer) {
  observers_.AddObserver(observer);
}

void CastConfigControllerMediaRouter::RemoveObserver(
    CastConfigController::Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool CastConfigControllerMediaRouter::HasMediaRouterForPrimaryProfile() const {
  return !!GetMediaRouter();
}

bool CastConfigControllerMediaRouter::HasSinksAndRoutes() const {
  return !devices_.empty();
}

bool CastConfigControllerMediaRouter::HasActiveRoute() const {
  for (const auto& device : devices_) {
    if (device.route.is_local_source && !device.route.title.empty())
      return true;
  }

  return false;
}

bool CastConfigControllerMediaRouter::AccessCodeCastingEnabled() const {
  Profile* profile = GetProfile();
  return base::FeatureList::IsEnabled(::features::kAccessCodeCastUI) &&
         profile && media_router::GetAccessCodeCastEnabledPref(profile);
}

void CastConfigControllerMediaRouter::RequestDeviceRefresh() {
  // The media router component isn't ready yet.
  if (!device_cache())
    return;

  // Build the old-style SinkAndRoute set out of the MediaRouter
  // source/sink/route setup. We first map the existing sinks, and then we
  // update those sinks with activity information.
  StopObservingMirroringMediaControllerHosts();
  UpdateDevices();

  for (auto& device : devices_) {
    if (device.route.id.size() > 0) {
      media_router::MirroringMediaControllerHost* freeze_host =
          GetMediaRouter()->GetMirroringMediaControllerHost(device.route.id);
      if (freeze_host) {
        freeze_host->AddObserver(this);
      }
    }
  }
}

const std::vector<ash::SinkAndRoute>&
CastConfigControllerMediaRouter::GetSinksAndRoutes() {
  return devices_;
}

void CastConfigControllerMediaRouter::CastToSink(const std::string& sink_id) {
  if (GetMediaRouter()) {
    // TODO(takumif): Pass in tab casting timeout.
    GetMediaRouter()->CreateRoute(
        media_router::MediaSource::ForUnchosenDesktop().id(), sink_id,
        url::Origin::Create(GURL("http://cros-cast-origin/")), nullptr,
        base::DoNothing(), base::TimeDelta());
  }
}

void CastConfigControllerMediaRouter::StopCasting(const std::string& route_id) {
  if (GetMediaRouter()) {
    GetMediaRouter()->TerminateRoute(route_id);
  }
}

void CastConfigControllerMediaRouter::FreezeRoute(const std::string& route_id) {
  if (!GetMediaRouter()) {
    return;
  }
  media_router::MirroringMediaControllerHost* freeze_host =
      GetMediaRouter()->GetMirroringMediaControllerHost(route_id);
  if (!freeze_host) {
    return;
  }
  freeze_host->Freeze();
}

void CastConfigControllerMediaRouter::UnfreezeRoute(
    const std::string& route_id) {
  if (!GetMediaRouter()) {
    return;
  }
  media_router::MirroringMediaControllerHost* freeze_host =
      GetMediaRouter()->GetMirroringMediaControllerHost(route_id);
  if (!freeze_host) {
    return;
  }
  freeze_host->Unfreeze();
}

void CastConfigControllerMediaRouter::OnUserProfileLoaded(
    const AccountId& account_id) {
  // The active profile has changed, which means that the media router has
  // as well. Reset the device cache to ensure we are using up-to-date
  // object instances.
  device_cache_.reset();
  RequestDeviceRefresh();
}

bool CastConfigControllerMediaRouter::IsAccessCodeCastFreezeUiEnabled() {
  Profile* profile = GetProfile();
  return profile && media_router::IsAccessCodeCastFreezeUiEnabled(profile);
}

void CastConfigControllerMediaRouter::
    StopObservingMirroringMediaControllerHosts() {
  for (const auto& device : devices_) {
    auto route_id = device.route.id;
    if (route_id.size() > 0) {
      media_router::MirroringMediaControllerHost* mirroring_controller_host =
          GetMediaRouter()->GetMirroringMediaControllerHost(route_id);
      if (mirroring_controller_host) {
        // It is safe to call RemoveObserver even if we are not observing a
        // particular host.
        mirroring_controller_host->RemoveObserver(this);
      }
    }
  }
}

void CastConfigControllerMediaRouter::UpdateDevices() {
  devices_.clear();

#if !defined(OFFICIAL_BUILD)
  // Optionally add fake cast devices for manual UI testing.
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          ash::switches::kQsAddFakeCastDevices)) {
    AddFakeCastDevices();
  }
#endif
  for (const media_router::MediaSink& sink : device_cache()->sinks()) {
    ash::SinkAndRoute device;
    device.sink.id = sink.id();
    device.sink.name = sink.name();
    device.sink.sink_icon_type =
        static_cast<ash::SinkIconType>(sink.icon_type());
    devices_.push_back(std::move(device));
  }

  for (const media_router::MediaRoute& route : device_cache()->routes()) {
    media_router::MirroringMediaControllerHost* freeze_host =
        IsAccessCodeCastFreezeUiEnabled()
            ? GetMediaRouter()->GetMirroringMediaControllerHost(
                  route.media_route_id())
            : nullptr;

    for (ash::SinkAndRoute& device : devices_) {
      if (device.sink.id == route.media_sink_id()) {
        device.route.id = route.media_route_id();
        device.route.title = route.description();
        device.route.is_local_source = route.is_local();

        // Only set freeze info if the appropriate feature is enabled. Else,
        // values default to false and freeze ui is not shown.
        if (freeze_host) {
          device.route.freeze_info.can_freeze = freeze_host->CanFreeze();
          device.route.freeze_info.is_frozen = freeze_host->IsFrozen();
        }

        // Default to a tab/app capture. This will display the media router
        // description. This means we will properly support DIAL casts.
        device.route.content_source =
            route.media_source().IsDesktopMirroringSource()
                ? ash::ContentSource::kDesktop
                : ash::ContentSource::kTab;
        break;
      }
    }
  }

  for (auto& observer : observers_)
    observer.OnDevicesUpdated(devices_);
}

#if !defined(OFFICIAL_BUILD)
void CastConfigControllerMediaRouter::AddFakeCastDevices() {
  // Add enough devices that the UI menu will scroll.
  for (int i = 1; i <= 10; i++) {
    ash::SinkAndRoute device;
    device.sink.id = "fake_sink_id_" + base::NumberToString(i);
    device.sink.name = "Fake Sink " + base::NumberToString(i);
    device.sink.sink_icon_type = ash::SinkIconType::kCast;
    devices_.push_back(std::move(device));
  }
}
#endif  // defined(OFFICIAL_BUILD)