blob: 465c01844d4d3137024d156a216cceded7302ff8 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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_delegate_media_router.h"
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/media/router/media_router.h"
#include "chrome/browser/media/router/media_router_factory.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/router/media_routes_observer.h"
#include "chrome/browser/media/router/media_sinks_observer.h"
#include "chrome/browser/media/router/media_source_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
namespace {
media_router::MediaRouter* media_router_for_test_ = nullptr;
// Returns the MediaRouter instance for the current primary profile.
media_router::MediaRouter* GetMediaRouter() {
if (media_router_for_test_)
return media_router_for_test_;
auto* router = media_router::MediaRouterFactory::GetApiForBrowserContext(
ProfileManager::GetPrimaryUserProfile());
DCHECK(router);
return router;
}
// The media router will sometimes append " (Tab)" to the tab title. This
// function will remove that data from the inout param |string|.
std::string StripEndingTab(const std::string& str) {
static const char ending[] = " (Tab)";
if (base::EndsWith(str, ending, base::CompareCase::SENSITIVE))
return str.substr(0, str.size() - strlen(ending));
return str;
}
} // 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(ash::CastConfigDelegate* cast_config_delegate);
~CastDeviceCache() override;
// This may call cast_config_delegate->RequestDeviceRefresh() 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,
const MediaRouteIds& unused_joinable_route_ids) override;
MediaSinks sinks_;
MediaRoutes routes_;
// Not owned.
ash::CastConfigDelegate* cast_config_delegate_;
DISALLOW_COPY_AND_ASSIGN(CastDeviceCache);
};
CastDeviceCache::CastDeviceCache(ash::CastConfigDelegate* cast_config_delegate)
: MediaRoutesObserver(GetMediaRouter()),
MediaSinksObserver(GetMediaRouter(),
media_router::MediaSourceForDesktop(),
GURL(chrome::kChromeUIMediaRouterURL)),
cast_config_delegate_(cast_config_delegate) {}
CastDeviceCache::~CastDeviceCache() {}
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;
// Hide all sinks which have a domain (ie, castouts) to meet privacy
// requirements. This will be enabled once UI can display the domain. See
// crbug.com/624016.
if (!sink.domain().empty())
continue;
sinks_.push_back(sink);
}
cast_config_delegate_->RequestDeviceRefresh();
}
void CastDeviceCache::OnRoutesUpdated(
const MediaRoutes& routes,
const MediaRouteIds& unused_joinable_route_ids) {
routes_ = routes;
cast_config_delegate_->RequestDeviceRefresh();
}
////////////////////////////////////////////////////////////////////////////////
// CastConfigDelegateMediaRouter:
void CastConfigDelegateMediaRouter::SetMediaRouterForTest(
media_router::MediaRouter* media_router) {
media_router_for_test_ = media_router;
}
CastConfigDelegateMediaRouter::CastConfigDelegateMediaRouter() {
// TODO(jdufault): This should use a callback interface once there is an
// equivalent. See crbug.com/666005.
registrar_.Add(this, chrome::NOTIFICATION_LOGIN_USER_PROFILE_PREPARED,
content::NotificationService::AllSources());
}
CastConfigDelegateMediaRouter::~CastConfigDelegateMediaRouter() {}
CastDeviceCache* CastConfigDelegateMediaRouter::devices() {
// The CastDeviceCache instance is lazily allocated because the MediaRouter
// component is not ready when the constructor is invoked.
if (!devices_ && GetMediaRouter()) {
devices_ = base::MakeUnique<CastDeviceCache>(this);
devices_->Init();
}
return devices_.get();
}
void CastConfigDelegateMediaRouter::RequestDeviceRefresh() {
// The media router component isn't ready yet.
if (!devices())
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.
SinksAndRoutes items;
for (const media_router::MediaSink& sink : devices()->sinks()) {
SinkAndRoute sr;
sr.sink.id = sink.id();
sr.sink.name = base::UTF8ToUTF16(sink.name());
sr.sink.domain = base::UTF8ToUTF16(sink.domain());
items.push_back(sr);
}
for (const media_router::MediaRoute& route : devices()->routes()) {
if (!route.for_display())
continue;
for (SinkAndRoute& item : items) {
if (item.sink.id == route.media_sink_id()) {
item.route.id = route.media_route_id();
item.route.title =
base::UTF8ToUTF16(StripEndingTab(route.description()));
item.route.is_local_source = route.is_local();
// Default to a tab/app capture. This will display the media router
// description. This means we will properly support DIAL casts.
item.route.content_source = Route::ContentSource::TAB;
if (media_router::IsDesktopMirroringMediaSource(route.media_source()))
item.route.content_source = Route::ContentSource::DESKTOP;
break;
}
}
}
for (ash::CastConfigDelegate::Observer& observer : observer_list_)
observer.OnDevicesUpdated(items);
}
void CastConfigDelegateMediaRouter::CastToSink(const Sink& sink) {
// TODO(imcheng): Pass in tab casting timeout.
GetMediaRouter()->CreateRoute(
media_router::MediaSourceForDesktop().id(), sink.id,
GURL("http://cros-cast-origin/"), nullptr,
std::vector<media_router::MediaRouteResponseCallback>(),
base::TimeDelta(), false);
}
void CastConfigDelegateMediaRouter::StopCasting(const Route& route) {
GetMediaRouter()->TerminateRoute(route.id);
}
void CastConfigDelegateMediaRouter::AddObserver(
ash::CastConfigDelegate::Observer* observer) {
observer_list_.AddObserver(observer);
}
void CastConfigDelegateMediaRouter::RemoveObserver(
ash::CastConfigDelegate::Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void CastConfigDelegateMediaRouter::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
switch (type) {
case chrome::NOTIFICATION_LOGIN_USER_PROFILE_PREPARED:
// 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.
devices_.reset();
RequestDeviceRefresh();
break;
}
}