Commit cacba2dc authored by Derek Cheng's avatar Derek Cheng Committed by Commit Bot

[DIAL MRP] DialActivityManager and DialInternalMessageUtil.

Part 3 of Custom DIAL launch patches. (Diffbase = 1014722)

Added the following classes:

DialActivityManager - handles custom DIAL launch session management
DialInternalMessageUtil - utility functions for generating/parsing
different types of Cast messages for Custom DIAL launch for
communicating with Cast SDK.

Bug: 816628
Change-Id: Id9eb4fdd393677771c9a278d78584e88339d21fa
Reviewed-on: https://chromium-review.googlesource.com/1018281
Commit-Queue: Derek Cheng <imcheng@chromium.org>
Reviewed-by: default avatarBrandon Tolsch <btolsch@chromium.org>
Cr-Commit-Position: refs/heads/master@{#554871}
parent cb4431a3
...@@ -99,6 +99,10 @@ static_library("router") { ...@@ -99,6 +99,10 @@ static_library("router") {
"providers/cast/chrome_cast_message_handler.h", "providers/cast/chrome_cast_message_handler.h",
"providers/cast/dual_media_sink_service.cc", "providers/cast/dual_media_sink_service.cc",
"providers/cast/dual_media_sink_service.h", "providers/cast/dual_media_sink_service.h",
"providers/dial/dial_activity_manager.cc",
"providers/dial/dial_activity_manager.h",
"providers/dial/dial_internal_message_util.cc",
"providers/dial/dial_internal_message_util.h",
"providers/dial/dial_media_route_provider.cc", "providers/dial/dial_media_route_provider.cc",
"providers/dial/dial_media_route_provider.h", "providers/dial/dial_media_route_provider.h",
"providers/extension/extension_media_route_provider_proxy.cc", "providers/extension/extension_media_route_provider_proxy.cc",
......
// Copyright 2018 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/media/router/providers/dial/dial_activity_manager.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/media/router/providers/dial/dial_internal_message_util.h"
#include "chrome/common/media_router/media_source_helper.h"
#include "net/base/url_util.h"
namespace media_router {
namespace {
// Returns the URL to use to launch |app_name| on |sink|.
GURL GetAppURL(const MediaSinkInternal& sink, const std::string& app_name) {
// The DIAL spec (Section 5.4) implies that the app URL must not have a
// trailing slash.
return GURL(sink.dial_data().app_url.spec() + "/" + app_name);
}
// Returns the Application Instance URL from the POST response headers given by
// |response_info|.
GURL GetApplicationInstanceURL(
const network::ResourceResponseHead& response_info) {
if (!response_info.headers)
return GURL();
// If the application is running after the action specified above, the DIAL
// server SHALL return an HTTP response with response code 201 Created. In
// this case, the LOCATION header of the response shall contain an absolute
// HTTP URL identifying the running instance of the application, known as
// the Application Instance URL. The host portion of the URL SHALL either
// resolve to an IPv4 address or be an IPv4 address. No response body shall
// be returned.
std::string location_header;
if (!response_info.headers->EnumerateHeader(nullptr, "LOCATION",
&location_header)) {
DVLOG(2) << "Missing LOCATION header";
return GURL();
}
GURL app_instance_url(location_header);
if (!app_instance_url.is_valid() || !app_instance_url.SchemeIs("http"))
return GURL();
return app_instance_url;
}
MediaRoute::Id GetMediaRouteId(const std::string& presentation_id,
const MediaSink::Id& sink_id,
const MediaSource& source) {
// TODO(https://crbug.com/816628): Can the route ID just be the presentation
// id?
return base::StringPrintf("urn:x-org.chromium:media:route:%s/%s/%s",
presentation_id.c_str(), sink_id.c_str(),
source.id().c_str());
}
} // namespace
DialLaunchInfo::DialLaunchInfo(const std::string& app_name,
const base::Optional<std::string>& post_data,
const std::string& client_id,
const GURL& app_launch_url)
: app_name(app_name),
post_data(post_data),
client_id(client_id),
app_launch_url(app_launch_url) {}
DialLaunchInfo::DialLaunchInfo(const DialLaunchInfo& other) = default;
DialLaunchInfo::~DialLaunchInfo() = default;
// static
std::unique_ptr<DialActivity> DialActivity::From(
const std::string& presentation_id,
const MediaSinkInternal& sink,
const MediaSource::Id& source_id,
bool incognito) {
MediaSource source(source_id);
GURL url = source.url();
if (!url.is_valid())
return nullptr;
std::string app_name = AppNameFromDialMediaSource(source);
if (app_name.empty())
return nullptr;
std::string client_id;
base::Optional<std::string> post_data;
// Note: QueryIterator stores the URL by reference, so we must not give it a
// temporary object.
for (net::QueryIterator query_it(url); !query_it.IsAtEnd();
query_it.Advance()) {
std::string key = query_it.GetKey();
if (key == "clientId") {
client_id = query_it.GetValue();
} else if (key == "dialPostData") {
post_data = query_it.GetValue();
}
}
if (client_id.empty())
return nullptr;
GURL app_launch_url = GetAppURL(sink, app_name);
DCHECK(app_launch_url.is_valid());
const MediaSink::Id& sink_id = sink.sink().id();
DialLaunchInfo launch_info(app_name, post_data, client_id, app_launch_url);
MediaRoute route(GetMediaRouteId(presentation_id, sink_id, source), source,
sink_id, app_name,
/* is_local */ true, /* for_display */ true);
route.set_presentation_id(presentation_id);
route.set_incognito(incognito);
return std::make_unique<DialActivity>(launch_info, route);
}
DialActivity::DialActivity(const DialLaunchInfo& launch_info,
const MediaRoute& route)
: launch_info(launch_info), route(route) {}
DialActivity::~DialActivity() = default;
DialActivityManager::DialActivityManager() = default;
DialActivityManager::~DialActivityManager() = default;
void DialActivityManager::AddActivity(const DialActivity& activity) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
MediaRoute::Id route_id = activity.route.media_route_id();
DCHECK(!base::ContainsKey(records_, route_id));
// TODO(https://crbug.com/816628): Consider adding a timeout for transitioning
// to kLaunched state to clean up unresponsive launches.
records_.emplace(route_id,
std::make_unique<DialActivityManager::Record>(activity));
}
const DialActivity* DialActivityManager::GetActivity(
const MediaRoute::Id& route_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = records_.find(route_id);
return record_it != records_.end() ? &(record_it->second->activity) : nullptr;
}
const DialActivity* DialActivityManager::GetActivityBySinkId(
const MediaSink::Id& sink_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = std::find_if(
records_.begin(), records_.end(), [&sink_id](const auto& record) {
return record.second->activity.route.media_sink_id() == sink_id;
});
return record_it != records_.end() ? &(record_it->second->activity) : nullptr;
}
void DialActivityManager::LaunchApp(
const MediaRoute::Id& route_id,
const CustomDialLaunchMessageBody& message,
DialActivityManager::LaunchAppCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = records_.find(route_id);
CHECK(record_it != records_.end());
auto& record = record_it->second;
if (record->pending_launch_request ||
record->state == DialActivityManager::Record::State::kLaunched)
return;
if (!message.do_launch) {
DVLOG(2) << "Launch will be handled by SDK client; skipping launch.";
record->state = DialActivityManager::Record::State::kLaunched;
std::move(callback).Run(true);
return;
}
const DialLaunchInfo& launch_info = record->activity.launch_info;
// |launch_parameter| overrides original POST data, if it exists.
const base::Optional<std::string>& post_data = message.launch_parameter
? message.launch_parameter
: launch_info.post_data;
// TODO(https://crbug.com/816628): Add metrics to record launch success/error.
auto fetcher =
CreateFetcher(base::BindOnce(&DialActivityManager::OnLaunchSuccess,
base::Unretained(this), route_id),
base::BindOnce(&DialActivityManager::OnLaunchError,
base::Unretained(this), route_id));
fetcher->Post(launch_info.app_launch_url, post_data);
record->pending_launch_request =
std::make_unique<DialActivityManager::DialLaunchRequest>(
std::move(fetcher), std::move(callback));
}
void DialActivityManager::StopApp(
const MediaRoute::Id& route_id,
mojom::MediaRouteProvider::TerminateRouteCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = records_.find(route_id);
if (record_it == records_.end()) {
DVLOG(2) << "Activity not found: " << route_id;
std::move(callback).Run("Activity not found",
RouteRequestResult::ROUTE_NOT_FOUND);
return;
}
auto& record = record_it->second;
if (record->pending_stop_request) {
std::move(callback).Run("A pending request already exists",
RouteRequestResult::UNKNOWN_ERROR);
return;
}
// Note that it is possible that the app launched on the device, but we
// haven't received the launch response yet. In this case we will treat it
// as if it never launched.
if (record->state != DialActivityManager::Record::State::kLaunched) {
DVLOG(2) << "App didn't launch; not issuing DELETE request.";
records_.erase(record_it);
std::move(callback).Run(base::nullopt, RouteRequestResult::OK);
return;
}
GURL app_instance_url = record->app_instance_url;
// If |app_instance_url| is not available, try a reasonable fallback.
if (!app_instance_url.is_valid()) {
const auto& activity = record->activity;
app_instance_url =
GURL(activity.launch_info.app_launch_url.spec() + "/run");
}
// TODO(https://crbug.com/816628): Add metrics to record stop success/error.
auto fetcher =
CreateFetcher(base::BindOnce(&DialActivityManager::OnStopSuccess,
base::Unretained(this), route_id),
base::BindOnce(&DialActivityManager::OnStopError,
base::Unretained(this), route_id));
fetcher->Delete(app_instance_url);
record->pending_stop_request =
std::make_unique<DialActivityManager::DialStopRequest>(
std::move(fetcher), std::move(callback));
}
std::vector<MediaRoute> DialActivityManager::GetRoutes() const {
std::vector<MediaRoute> routes;
for (const auto& record : records_)
routes.push_back(record.second->activity.route);
return routes;
}
std::unique_ptr<DialURLFetcher> DialActivityManager::CreateFetcher(
DialURLFetcher::SuccessCallback success_cb,
DialURLFetcher::ErrorCallback error_cb) {
// TODO(https://crbug.com/816628): Add timeout.
return std::make_unique<DialURLFetcher>(std::move(success_cb),
std::move(error_cb));
}
void DialActivityManager::OnLaunchSuccess(const MediaRoute::Id& route_id,
const std::string& response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = records_.find(route_id);
if (record_it == records_.end())
return;
auto& record = record_it->second;
const network::ResourceResponseHead* response_info =
record->pending_launch_request->fetcher->GetResponseHead();
DCHECK(response_info);
record->app_instance_url = GetApplicationInstanceURL(*response_info);
record->state = DialActivityManager::Record::State::kLaunched;
std::move(record->pending_launch_request->callback).Run(true);
record->pending_launch_request.reset();
}
void DialActivityManager::OnLaunchError(const MediaRoute::Id& route_id,
int response_code,
const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DVLOG(2) << "Response code: " << response_code << ", message: " << message;
auto record_it = records_.find(route_id);
if (record_it == records_.end())
return;
// Clean up the activity / route.
std::move(record_it->second->pending_launch_request->callback).Run(false);
records_.erase(record_it);
}
void DialActivityManager::OnStopSuccess(const MediaRoute::Id& route_id,
const std::string& response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto record_it = records_.find(route_id);
if (record_it == records_.end())
return;
// Move the callback out of the record since we are erasing the record.
auto& record = record_it->second;
auto cb = std::move(record->pending_stop_request->callback);
records_.erase(record_it);
std::move(cb).Run(base::nullopt, RouteRequestResult::OK);
}
void DialActivityManager::OnStopError(const MediaRoute::Id& route_id,
int response_code,
const std::string& message) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DVLOG(2) << "Response code: " << response_code << ", message: " << message;
auto record_it = records_.find(route_id);
if (record_it == records_.end())
return;
// Move the callback out of the record since we are erasing the record.
auto& record = record_it->second;
auto cb = std::move(record->pending_stop_request->callback);
record->pending_stop_request.reset();
std::move(cb).Run(message, RouteRequestResult::UNKNOWN_ERROR);
}
DialActivityManager::Record::Record(const DialActivity& activity)
: activity(activity) {}
DialActivityManager::Record::~Record() = default;
} // namespace media_router
// Copyright 2018 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.
#ifndef CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_ACTIVITY_MANAGER_H_
#define CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_ACTIVITY_MANAGER_H_
#include <memory>
#include <string>
#include "base/containers/flat_map.h"
#include "base/sequence_checker.h"
#include "chrome/browser/media/router/discovery/dial/dial_url_fetcher.h"
#include "chrome/common/media_router/discovery/media_sink_internal.h"
#include "chrome/common/media_router/media_route.h"
#include "chrome/common/media_router/media_source.h"
#include "chrome/common/media_router/mojo/media_router.mojom.h"
#include "chrome/common/media_router/route_request_result.h"
#include "url/gurl.h"
namespace media_router {
struct CustomDialLaunchMessageBody;
// Represents parameters used to complete a custom DIAL launch sequence.
struct DialLaunchInfo {
DialLaunchInfo(const std::string& app_name,
const base::Optional<std::string>& post_data,
const std::string& client_id,
const GURL& app_launch_url);
DialLaunchInfo(const DialLaunchInfo& other);
~DialLaunchInfo();
std::string app_name;
// The data to include with the app launch POST request. Note this may be
// overridden by the launchParameter in the CUSTOM_DIAL_LAUNCH response.
base::Optional<std::string> post_data;
// Cast SDK client ID.
std::string client_id;
// The URL to use to launch the app.
GURL app_launch_url;
};
// Represents custom DIAL launch activity that is to be or has been initiated
// on a DIAL MediaSink.
struct DialActivity {
public:
// Returns a DialActivity object created from the given parameters. Returns
// nullptr if |source_id| does not represent a valid DIAL MediaSource.
static std::unique_ptr<DialActivity> From(const std::string& presentation_id,
const MediaSinkInternal& sink,
const MediaSource::Id& source_id,
bool incognito);
DialActivity(const DialLaunchInfo& launch_info, const MediaRoute& route);
~DialActivity();
DialLaunchInfo launch_info;
// TODO(https://crbug.com/816628): The MediaRoute itself does not contain
// sufficient information to tell the current state of the activity (launching
// vs. launched). Because of this, the route is rendered in the Media Router
// UI the same way for both states. Consider introducing a state property in
// MediaRoute so that the UI can render them differently.
MediaRoute route;
};
template <typename CallbackType>
struct DialPendingRequest {
public:
DialPendingRequest(std::unique_ptr<DialURLFetcher> fetcher,
CallbackType callback)
: fetcher(std::move(fetcher)), callback(std::move(callback)) {}
~DialPendingRequest() = default;
std::unique_ptr<DialURLFetcher> fetcher;
CallbackType callback;
DISALLOW_COPY_AND_ASSIGN(DialPendingRequest);
};
// Keeps track of all ongoing custom DIAL launch activities, and is the source
// of truth for DialActivity and MediaRoutes for the DIAL MRP.
// Custom DIAL launch consists of a sequence of message excahnges between the
// MRP and the Cast SDK client:
// (1) When the user requests a custom DIAL launch, |AddActivity()| is called
// with a DialActivity pertaining to the launch. This creates a MediaRoute and
// a connection between the MRP and the SDK client to begin message exchanges.
// (2) The SDK client sends back a custom DIAL launch response. |LaunchApp()|
// is called with the response message which contains additional launch
// parameters.
// (3) After the app is launched, |StopApp()| may be called to terminate the
// app on the receiver.
// All methods on this class must run on the same sequence.
// TODO(crbug.com/816628): We should be able to simplify the interaction
// between DialMediaRouteProvider and this class once PresentationConnection is
// available to this class to communicate with the page directly.
class DialActivityManager {
public:
using LaunchAppCallback = base::OnceCallback<void(bool)>;
DialActivityManager();
virtual ~DialActivityManager();
// Adds |activity| to the manager. This call is valid only if there is no
// existing activity with the same MediaRoute ID.
void AddActivity(const DialActivity& activity);
// Returns the DialActivity associated with |route_id| or nullptr if not
// found.
const DialActivity* GetActivity(const MediaRoute::Id& route_id) const;
// Returns the DialActivity associated with |sink_id| or nullptr if not
// found.
const DialActivity* GetActivityBySinkId(const MediaSink::Id& sink_id) const;
// Launches the app specified in the activity associated with |route_id|.
// If |message.launch_parameter| is set, then it overrides the post data
// originally specified in the activity's DialLaunchInfo.
// |callback| will be invoked with |true| if the launch succeeded, or |false|
// if the launch failed.
// If |message.do_launch| is |false|, then app launch will be skipped, and
// |calback| will be invoked with |true|.
// This method is only valid to call if there is an activity for |route_id|.
// This method is a no-op if there is already a pending launch request, or
// if the app is already launched.
void LaunchApp(const MediaRoute::Id& route_id,
const CustomDialLaunchMessageBody& message,
LaunchAppCallback callback);
// Stops the app that is currently active on |route_id|.
// On success, the associated DialActivity and MediaRoute will be removed
// before |callback| is invoked. On failure, the DialActivity and MediaRoute
// will not be removed.
void StopApp(const MediaRoute::Id& route_id,
mojom::MediaRouteProvider::TerminateRouteCallback callback);
std::vector<MediaRoute> GetRoutes() const;
private:
using DialLaunchRequest =
DialPendingRequest<DialActivityManager::LaunchAppCallback>;
using DialStopRequest =
DialPendingRequest<mojom::MediaRouteProvider::TerminateRouteCallback>;
// Represents the state of a launch activity.
struct Record {
explicit Record(const DialActivity& activity);
~Record();
enum State { kLaunching, kLaunched };
const DialActivity activity;
GURL app_instance_url;
std::unique_ptr<DialLaunchRequest> pending_launch_request;
std::unique_ptr<DialStopRequest> pending_stop_request;
State state = kLaunching;
DISALLOW_COPY_AND_ASSIGN(Record);
};
// Marked virtual for tests.
virtual std::unique_ptr<DialURLFetcher> CreateFetcher(
DialURLFetcher::SuccessCallback success_cb,
DialURLFetcher::ErrorCallback error_cb);
void OnLaunchSuccess(const MediaRoute::Id& route_id,
const std::string& response);
void OnLaunchError(const MediaRoute::Id& route_id,
int response_code,
const std::string& message);
void OnStopSuccess(const MediaRoute::Id& route_id,
const std::string& response);
void OnStopError(const MediaRoute::Id& route_id,
int response_code,
const std::string& message);
base::flat_map<MediaRoute::Id, std::unique_ptr<Record>> records_;
SEQUENCE_CHECKER(sequence_checker_);
DISALLOW_COPY_AND_ASSIGN(DialActivityManager);
};
} // namespace media_router
#endif // CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_ACTIVITY_MANAGER_H_
// Copyright 2018 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/media/router/providers/dial/dial_activity_manager.h"
#include "base/test/scoped_task_environment.h"
#include "chrome/browser/media/router/providers/dial/dial_internal_message_util.h"
#include "chrome/browser/media/router/test/test_helper.h"
#include "net/http/http_status_code.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
using testing::Not;
namespace media_router {
TEST(DialActivityTest, From) {
std::string presentation_id = "presentationId";
MediaSinkInternal sink = CreateDialSink(1);
MediaSource::Id source_id =
"cast-dial:YouTube?clientId=152127444812943594&dialPostData=foo";
auto activity =
DialActivity::From(presentation_id, sink, source_id, /*incognito*/ true);
ASSERT_TRUE(activity);
GURL expected_app_launch_url(sink.dial_data().app_url.spec() + "/YouTube");
const DialLaunchInfo& launch_info = activity->launch_info;
EXPECT_EQ("YouTube", launch_info.app_name);
EXPECT_EQ("152127444812943594", launch_info.client_id);
EXPECT_EQ("foo", launch_info.post_data);
EXPECT_EQ(expected_app_launch_url, launch_info.app_launch_url);
const MediaRoute& route = activity->route;
EXPECT_EQ(presentation_id, route.presentation_id());
EXPECT_EQ(source_id, route.media_source().id());
EXPECT_EQ(sink.sink().id(), route.media_sink_id());
EXPECT_EQ("YouTube", route.description());
EXPECT_TRUE(route.is_local());
EXPECT_TRUE(route.is_incognito());
EXPECT_FALSE(route.is_local_presentation());
EXPECT_EQ(RouteControllerType::kNone, route.controller_type());
}
class DialActivityManagerTest : public testing::Test {
public:
DialActivityManagerTest() : manager_(&loader_factory_) {}
~DialActivityManagerTest() override = default;
void TestLaunchApp(const DialActivity& activity,
const base::Optional<std::string>& launch_parameter,
const base::Optional<GURL>& app_instance_url) {
manager_.SetExpectedRequest(activity.launch_info.app_launch_url, "POST",
launch_parameter ? *launch_parameter : "foo");
LaunchApp(activity.route.media_route_id(), launch_parameter);
// |GetRoutes()| returns the route even though it is still launching.
EXPECT_FALSE(manager_.GetRoutes().empty());
// Pending launch request, no-op.
EXPECT_CALL(manager_, OnFetcherCreated()).Times(0);
LaunchApp(activity.route.media_route_id(), base::nullopt);
LaunchApp(activity.route.media_route_id(), "bar");
network::ResourceResponseHead response_head;
if (app_instance_url) {
response_head.headers =
base::MakeRefCounted<net::HttpResponseHeaders>("");
response_head.headers->AddHeader("LOCATION: " + app_instance_url->spec());
}
loader_factory_.AddResponse(activity.launch_info.app_launch_url,
response_head, "",
network::URLLoaderCompletionStatus());
EXPECT_CALL(*this, OnAppLaunchResult(true));
base::RunLoop().RunUntilIdle();
auto routes = manager_.GetRoutes();
EXPECT_EQ(1u, routes.size());
EXPECT_TRUE(routes[0].Equals(activity.route));
// App already launched, no-op.
EXPECT_CALL(manager_, OnFetcherCreated()).Times(0);
LaunchApp(activity.route.media_route_id(), base::nullopt);
}
void LaunchApp(const MediaRoute::Id& route_id,
const base::Optional<std::string>& launch_parameter) {
CustomDialLaunchMessageBody message(true, launch_parameter);
manager_.LaunchApp(
route_id, message,
base::BindOnce(&DialActivityManagerTest::OnAppLaunchResult,
base::Unretained(this)));
}
MOCK_METHOD1(OnAppLaunchResult, void(bool));
void StopApp(const MediaRoute::Id& route_id) {
manager_.StopApp(route_id,
base::BindOnce(&DialActivityManagerTest::OnStopAppResult,
base::Unretained(this)));
}
MOCK_METHOD2(OnStopAppResult,
void(const base::Optional<std::string>&,
RouteRequestResult::ResultCode));
protected:
base::test::ScopedTaskEnvironment environment_;
std::string presentation_id_ = "presentationId";
MediaSinkInternal sink_ = CreateDialSink(1);
MediaSource::Id source_id_ =
"cast-dial:YouTube?clientId=152127444812943594&dialPostData=foo";
network::TestURLLoaderFactory loader_factory_;
TestDialActivityManager manager_;
DISALLOW_COPY_AND_ASSIGN(DialActivityManagerTest);
};
TEST_F(DialActivityManagerTest, AddActivity) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
EXPECT_TRUE(manager_.GetRoutes().empty());
manager_.AddActivity(*activity);
// |GetRoutes()| returns the route even though it is still launching.
EXPECT_FALSE(manager_.GetRoutes().empty());
EXPECT_TRUE(manager_.GetActivity(activity->route.media_route_id()));
}
TEST_F(DialActivityManagerTest, LaunchApp) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
GURL app_instance_url =
GURL(sink_.dial_data().app_url.spec() + "/YouTube/app_instance");
TestLaunchApp(*activity, base::nullopt, app_instance_url);
}
TEST_F(DialActivityManagerTest, LaunchAppLaunchParameter) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
GURL app_instance_url =
GURL(sink_.dial_data().app_url.spec() + "/YouTube/app_instance");
TestLaunchApp(*activity, "bar", app_instance_url);
}
TEST_F(DialActivityManagerTest, LaunchAppFails) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
manager_.SetExpectedRequest(activity->launch_info.app_launch_url, "POST",
"foo");
LaunchApp(activity->route.media_route_id(), base::nullopt);
network::ResourceResponseHead response_head;
loader_factory_.AddResponse(
activity->launch_info.app_launch_url, response_head, "",
network::URLLoaderCompletionStatus(net::HTTP_SERVICE_UNAVAILABLE));
EXPECT_CALL(*this, OnAppLaunchResult(false));
base::RunLoop().RunUntilIdle();
// Activity is removed on failure.
EXPECT_TRUE(manager_.GetRoutes().empty());
EXPECT_FALSE(manager_.GetActivity(activity->route.media_route_id()));
}
TEST_F(DialActivityManagerTest, StopApp) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
GURL app_instance_url =
GURL(sink_.dial_data().app_url.spec() + "/YouTube/app_instance");
TestLaunchApp(*activity, base::nullopt, app_instance_url);
manager_.SetExpectedRequest(app_instance_url, "DELETE", base::nullopt);
StopApp(activity->route.media_route_id());
// Pending stop request.
EXPECT_CALL(*this, OnStopAppResult(_, Not(RouteRequestResult::OK)));
EXPECT_CALL(manager_, OnFetcherCreated()).Times(0);
StopApp(activity->route.media_route_id());
loader_factory_.AddResponse(app_instance_url, network::ResourceResponseHead(),
"", network::URLLoaderCompletionStatus());
EXPECT_CALL(*this, OnStopAppResult(testing::Eq(base::nullopt),
RouteRequestResult::OK));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(manager_.GetRoutes().empty());
}
TEST_F(DialActivityManagerTest, StopAppUseFallbackURL) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
TestLaunchApp(*activity, base::nullopt, base::nullopt);
GURL app_instance_url =
GURL(activity->launch_info.app_launch_url.spec() + "/run");
manager_.SetExpectedRequest(app_instance_url, "DELETE", base::nullopt);
StopApp(activity->route.media_route_id());
loader_factory_.AddResponse(app_instance_url, network::ResourceResponseHead(),
"", network::URLLoaderCompletionStatus());
EXPECT_CALL(*this, OnStopAppResult(testing::Eq(base::nullopt),
RouteRequestResult::OK));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(manager_.GetRoutes().empty());
}
TEST_F(DialActivityManagerTest, StopAppFails) {
auto activity = DialActivity::From(presentation_id_, sink_, source_id_,
/*incognito*/ false);
ASSERT_TRUE(activity);
manager_.AddActivity(*activity);
TestLaunchApp(*activity, base::nullopt, base::nullopt);
GURL app_instance_url =
GURL(activity->launch_info.app_launch_url.spec() + "/run");
manager_.SetExpectedRequest(app_instance_url, "DELETE", base::nullopt);
StopApp(activity->route.media_route_id());
loader_factory_.AddResponse(
app_instance_url, network::ResourceResponseHead(), "",
network::URLLoaderCompletionStatus(net::HTTP_SERVICE_UNAVAILABLE));
EXPECT_CALL(*this, OnStopAppResult(_, Not(RouteRequestResult::OK)));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(manager_.GetActivity(activity->route.media_route_id()));
EXPECT_FALSE(manager_.GetRoutes().empty());
}
} // namespace media_router
// Copyright 2018 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/media/router/providers/dial/dial_internal_message_util.h"
#include <array>
#include "base/base64url.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/sha1.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/media/router/providers/dial/dial_activity_manager.h"
#include "chrome/common/media_router/discovery/media_sink_internal.h"
#include "net/base/escape.h"
#include "url/url_util.h"
namespace media_router {
namespace {
constexpr int kSequenceNumberWrap = 2 << 30;
int GetNextDialLaunchSequenceNumber() {
static int next_seq_number = 0;
next_seq_number = (next_seq_number + 1) % kSequenceNumberWrap;
return next_seq_number;
}
std::string GetNextSessionId() {
static int session_id = 0;
session_id = (session_id + 1) % kSequenceNumberWrap;
return base::NumberToString(session_id);
}
std::string DialInternalMessageTypeToString(DialInternalMessageType type) {
switch (type) {
case DialInternalMessageType::kClientConnect:
return "client_connect";
case DialInternalMessageType::kV2Message:
return "v2_message";
case DialInternalMessageType::kReceiverAction:
return "receiver_action";
case DialInternalMessageType::kNewSession:
return "new_session";
case DialInternalMessageType::kCustomDialLaunch:
return "custom_dial_launch";
case DialInternalMessageType::kOther:
break;
}
NOTREACHED() << "Unknown message type: " << static_cast<int>(type);
return "unknown";
}
DialInternalMessageType StringToDialInternalMessageType(
const std::string& str_type) {
if (str_type == "client_connect")
return DialInternalMessageType::kClientConnect;
if (str_type == "v2_message")
return DialInternalMessageType::kV2Message;
if (str_type == "receiver_action")
return DialInternalMessageType::kReceiverAction;
if (str_type == "new_session")
return DialInternalMessageType::kNewSession;
if (str_type == "custom_dial_launch")
return DialInternalMessageType::kCustomDialLaunch;
return DialInternalMessageType::kOther;
}
base::Value CreateReceiver(const MediaSinkInternal& sink) {
base::Value receiver(base::Value::Type::DICTIONARY);
std::string label = base::SHA1HashString(sink.sink().id());
base::Base64UrlEncode(label, base::Base64UrlEncodePolicy::OMIT_PADDING,
&label);
receiver.SetKey("label", base::Value(label));
receiver.SetKey("friendlyName",
base::Value(net::EscapeForHTML(sink.sink().name())));
receiver.SetKey("capabilities", base::ListValue());
receiver.SetKey("volume", base::Value(base::Value::Type::NONE));
receiver.SetKey("isActiveInput", base::Value(base::Value::Type::NONE));
receiver.SetKey("displayStatus", base::Value(base::Value::Type::NONE));
receiver.SetKey("receiverType", base::Value("dial"));
receiver.SetKey("ipAddress",
base::Value(sink.dial_data().ip_address.ToString()));
return receiver;
}
std::string DialReceiverActionToString(DialReceiverAction action) {
switch (action) {
case DialReceiverAction::kCast:
return "cast";
case DialReceiverAction::kStop:
return "stop";
}
NOTREACHED() << "Unknown DialReceiverAction: " << static_cast<int>(action);
return "";
}
base::Value CreateReceiverActionBody(const MediaSinkInternal& sink,
DialReceiverAction action) {
base::Value message_body(base::Value::Type::DICTIONARY);
message_body.SetKey("receiver", CreateReceiver(sink));
message_body.SetKey("action",
base::Value(DialReceiverActionToString(action)));
return message_body;
}
base::Value CreateNewSessionBody(const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink) {
base::Value message_body(base::Value::Type::DICTIONARY);
message_body.SetKey("sessionId", base::Value(GetNextSessionId()));
message_body.SetKey("appId", base::Value(""));
message_body.SetKey("displayName", base::Value(launch_info.app_name));
message_body.SetKey("statusText", base::Value(""));
message_body.SetKey("appImages", base::ListValue());
message_body.SetKey("receiver", CreateReceiver(sink));
message_body.SetKey("senderApps", base::ListValue());
message_body.SetKey("namespaces", base::ListValue());
message_body.SetKey("media", base::ListValue());
message_body.SetKey("status", base::Value("connected"));
message_body.SetKey("transportId", base::Value(""));
return message_body;
}
base::Value CreateCustomDialLaunchBody(const MediaSinkInternal& sink,
const ParsedDialAppInfo& app_info) {
base::Value message_body(base::Value::Type::DICTIONARY);
message_body.SetKey("receiver", CreateReceiver(sink));
message_body.SetKey("appState",
base::Value(DialAppStateToString(app_info.state)));
if (!app_info.extra_data.empty()) {
base::Value extra_data(base::Value::Type::DICTIONARY);
for (const auto& key_value : app_info.extra_data)
message_body.SetKey(key_value.first, base::Value(key_value.second));
message_body.SetKey("extraData", std::move(extra_data));
}
return message_body;
}
base::Value CreateDialMessageCommon(DialInternalMessageType type,
base::Value body,
const std::string& client_id) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("type", base::Value(DialInternalMessageTypeToString(type)));
message.SetKey("message", std::move(body));
message.SetKey("clientId", base::Value(client_id));
message.SetKey("sequenceNumber", base::Value(-1));
message.SetKey("timeoutMillis", base::Value(0));
return message;
}
} // namespace
// static
std::unique_ptr<DialInternalMessage> DialInternalMessage::From(
const std::string& message) {
// TODO(https://crbug.com/816628): This may need to be parsed out of process.
std::unique_ptr<base::Value> message_value = base::JSONReader::Read(message);
if (!message_value) {
DVLOG(2) << "Failed to read JSON message: " << message;
return nullptr;
}
base::Value* type_value = message_value->FindKey("type");
if (!type_value || !type_value->is_string()) {
DVLOG(2) << "Missing type value";
return nullptr;
}
std::string str_type = type_value->GetString();
DialInternalMessageType message_type =
StringToDialInternalMessageType(str_type);
if (message_type == DialInternalMessageType::kOther) {
DVLOG(2) << __func__ << ": Unsupported message type: " << str_type;
return nullptr;
}
base::Value* client_id_value = message_value->FindKey("clientId");
if (!client_id_value || !client_id_value->is_string()) {
DVLOG(2) << "Missing clientId";
return nullptr;
}
// "message" is optional.
base::Optional<base::Value> message_body;
base::Value* message_body_value = message_value->FindKey("message");
if (message_body_value)
message_body = message_body_value->Clone();
int sequence_number = -1;
base::Value* sequence_number_value = message_value->FindKey("sequenceNumber");
if (sequence_number_value && sequence_number_value->is_int())
sequence_number = sequence_number_value->GetInt();
return std::make_unique<DialInternalMessage>(
message_type, std::move(message_body), client_id_value->GetString(),
sequence_number);
}
DialInternalMessage::DialInternalMessage(DialInternalMessageType type,
base::Optional<base::Value> body,
const std::string& client_id,
int sequence_number)
: type(type),
body(std::move(body)),
client_id(client_id),
sequence_number(sequence_number) {}
DialInternalMessage::~DialInternalMessage() = default;
// static
CustomDialLaunchMessageBody CustomDialLaunchMessageBody::From(
const DialInternalMessage& message) {
DCHECK(message.type == DialInternalMessageType::kCustomDialLaunch);
const base::Optional<base::Value>& body = message.body;
if (!body)
return CustomDialLaunchMessageBody();
const base::Value* do_launch_value = body->FindKey("doLaunch");
if (!do_launch_value || !do_launch_value->is_bool())
return CustomDialLaunchMessageBody();
bool do_launch = do_launch_value->GetBool();
base::Optional<std::string> launch_parameter;
const base::Value* launch_parameter_value = body->FindKey("launchParameter");
if (launch_parameter_value && launch_parameter_value->is_string())
launch_parameter = launch_parameter_value->GetString();
return CustomDialLaunchMessageBody(do_launch, launch_parameter);
}
CustomDialLaunchMessageBody::CustomDialLaunchMessageBody() = default;
CustomDialLaunchMessageBody::CustomDialLaunchMessageBody(
bool do_launch,
const base::Optional<std::string>& launch_parameter)
: do_launch(do_launch), launch_parameter(launch_parameter) {}
CustomDialLaunchMessageBody::CustomDialLaunchMessageBody(
const CustomDialLaunchMessageBody& other) = default;
CustomDialLaunchMessageBody::~CustomDialLaunchMessageBody() = default;
// static
bool DialInternalMessageUtil::IsStopSessionMessage(
const DialInternalMessage& message) {
if (message.type != DialInternalMessageType::kV2Message)
return false;
if (!message.body)
return false;
const base::Value* request_type = message.body->FindKey("type");
return request_type && request_type->is_string() &&
request_type->GetString() == "STOP";
}
// static
content::PresentationConnectionMessage
DialInternalMessageUtil::CreateNewSessionMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink) {
base::Value message = CreateDialMessageCommon(
DialInternalMessageType::kNewSession,
CreateNewSessionBody(launch_info, sink), launch_info.client_id);
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return content::PresentationConnectionMessage(std::move(str));
}
// static
content::PresentationConnectionMessage
DialInternalMessageUtil::CreateReceiverActionCastMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink) {
base::Value message = CreateDialMessageCommon(
DialInternalMessageType::kReceiverAction,
CreateReceiverActionBody(sink, DialReceiverAction::kCast),
launch_info.client_id);
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return content::PresentationConnectionMessage(std::move(str));
}
// static
content::PresentationConnectionMessage
DialInternalMessageUtil::CreateReceiverActionStopMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink) {
base::Value message = CreateDialMessageCommon(
DialInternalMessageType::kReceiverAction,
CreateReceiverActionBody(sink, DialReceiverAction::kStop),
launch_info.client_id);
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return content::PresentationConnectionMessage(std::move(str));
}
// static
std::pair<content::PresentationConnectionMessage, int>
DialInternalMessageUtil::CreateCustomDialLaunchMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink,
const ParsedDialAppInfo& app_info) {
int seq_number = GetNextDialLaunchSequenceNumber();
base::Value message = CreateDialMessageCommon(
DialInternalMessageType::kCustomDialLaunch,
CreateCustomDialLaunchBody(sink, app_info), launch_info.client_id);
message.SetKey("sequenceNumber", base::Value(seq_number));
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return {content::PresentationConnectionMessage(std::move(str)), seq_number};
}
} // namespace media_router
// Copyright 2018 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.
#ifndef CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_INTERNAL_MESSAGE_UTIL_H_
#define CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_INTERNAL_MESSAGE_UTIL_H_
#include <memory>
#include <string>
#include "base/values.h"
#include "chrome/browser/media/router/discovery/dial/parsed_dial_app_info.h"
#include "content/public/common/presentation_connection_message.h"
namespace media_router {
struct DialLaunchInfo;
class MediaSinkInternal;
// Types of internal messages that are used in a custom DIAL launch workflow.
enum class DialInternalMessageType {
// Cast SDK -> MR
kClientConnect,
kV2Message,
// MR -> Cast SDK
kNewSession,
kReceiverAction,
// MR <-> Cast SDK
kCustomDialLaunch,
kOther
};
// Possible types of ReceiverAction taken by the user on a receiver.
enum class DialReceiverAction {
// The user selected a receiver with the intent of casting to it with the
// sender application.
kCast,
// The user requested to stop the session running on a receiver.
kStop
};
// Parsed custom DIAL launch internal message coming from a Cast SDK client.
struct DialInternalMessage {
// Returns a DialInternalMessage for |message|, or nullptr is |message| is not
// a valid custom DIAL launch internal message.
static std::unique_ptr<DialInternalMessage> From(const std::string& message);
DialInternalMessage(DialInternalMessageType type,
base::Optional<base::Value> body,
const std::string& client_id,
int sequence_number);
~DialInternalMessage();
DialInternalMessageType type;
base::Optional<base::Value> body;
std::string client_id;
int sequence_number;
DISALLOW_COPY_AND_ASSIGN(DialInternalMessage);
};
// Parsed CUSTOM_DIAL_LAUNCH response from the Cast SDK client.
struct CustomDialLaunchMessageBody {
// Returns a CustomDialLaunchMessageBody for |message|.
// This method is only valid to call if |message.type| == |kCustomDialLaunch|.
static CustomDialLaunchMessageBody From(const DialInternalMessage& message);
CustomDialLaunchMessageBody();
CustomDialLaunchMessageBody(
bool do_launch,
const base::Optional<std::string>& launch_parameter);
CustomDialLaunchMessageBody(const CustomDialLaunchMessageBody& other);
~CustomDialLaunchMessageBody();
// If |true|, the DialMediaRouteProvider should handle the app launch.
bool do_launch = true;
// If |do_launch| is |true|, optional launch parameter to include with the
// launch (POST) request. This overrides the launch parameter that was
// specified in the MediaSource (if any).
base::Optional<std::string> launch_parameter;
};
class DialInternalMessageUtil {
public:
// Returns |true| if |message| is a valid STOP_SESSION message.
static bool IsStopSessionMessage(const DialInternalMessage& message);
// Returns a NEW_SESSION message to be sent to the page when the user requests
// an app launch.
static content::PresentationConnectionMessage CreateNewSessionMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink);
// Returns a RECEIVER_ACTION / CAST message to be sent to the page when the
// user requests an app launch.
static content::PresentationConnectionMessage CreateReceiverActionCastMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink);
// Returns a RECEIVER_ACTION / STOP message to be sent to the page when an app
// is stopped by DialMediaRouteProvider.
static content::PresentationConnectionMessage CreateReceiverActionStopMessage(
const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink);
// Returns a CUSTOM_DIAL_LAUNCH request message to be sent to the page.
// Generates and returns the next number to associate a DIAL launch sequence
// with.
static std::pair<content::PresentationConnectionMessage, int>
CreateCustomDialLaunchMessage(const DialLaunchInfo& launch_info,
const MediaSinkInternal& sink,
const ParsedDialAppInfo& app_info);
};
} // namespace media_router
#endif // CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_DIAL_DIAL_INTERNAL_MESSAGE_UTIL_H_
// Copyright 2018 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/media/router/providers/dial/dial_internal_message_util.h"
#include "base/json/json_reader.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/media/router/providers/dial/dial_activity_manager.h"
#include "chrome/browser/media/router/test/test_helper.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media_router {
class DialInternalMessageUtilTest : public ::testing::Test {
public:
DialInternalMessageUtilTest()
: launch_info_("YouTube",
base::nullopt,
"152127444812943594",
GURL("http://172.17.32.151/app/YouTube")) {
MediaSink sink("dial:<29a400068c051073801508058128105d>", "Lab Roku",
SinkIconType::GENERIC);
DialSinkExtraData extra_data;
extra_data.ip_address = net::IPAddress(172, 17, 32, 151);
sink_ = MediaSinkInternal(sink, extra_data);
}
void ExpectMessagesEqual(const std::string& expected_message,
const std::string& message) {
auto expected_message_value = base::JSONReader::Read(expected_message);
ASSERT_TRUE(expected_message_value);
auto message_value = base::JSONReader::Read(message);
ASSERT_TRUE(message_value);
EXPECT_EQ(*expected_message_value, *message_value);
}
protected:
DialLaunchInfo launch_info_;
MediaSinkInternal sink_;
};
TEST_F(DialInternalMessageUtilTest, ParseClientConnectMessage) {
const char kClientConnectMessage[] = R"(
{
"type":"client_connect",
"message":"15212681945883010",
"sequenceNumber":-1,
"timeoutMillis":0,
"clientId":"15212681945883010"
})";
auto message = DialInternalMessage::From(kClientConnectMessage);
ASSERT_TRUE(message);
EXPECT_EQ(DialInternalMessageType::kClientConnect, message->type);
EXPECT_EQ(base::Value("15212681945883010"), message->body);
EXPECT_EQ("15212681945883010", message->client_id);
EXPECT_EQ(-1, message->sequence_number);
}
TEST_F(DialInternalMessageUtilTest, ParseCustomDialLaunchMessage) {
const char kCustomDialLaunchMessage[] = R"(
{
"type":"custom_dial_launch",
"message": {
"doLaunch":true,
"launchParameter":"pairingCode=foo"
},
"sequenceNumber":12345,
"timeoutMillis":3000,
"clientId":"152127444812943594"
})";
auto message = DialInternalMessage::From(kCustomDialLaunchMessage);
ASSERT_TRUE(message);
EXPECT_EQ(DialInternalMessageType::kCustomDialLaunch, message->type);
EXPECT_EQ("152127444812943594", message->client_id);
EXPECT_EQ(12345, message->sequence_number);
CustomDialLaunchMessageBody body =
CustomDialLaunchMessageBody::From(*message);
EXPECT_TRUE(body.do_launch);
EXPECT_EQ("pairingCode=foo", body.launch_parameter);
}
TEST_F(DialInternalMessageUtilTest, ParseV2StopSessionMessage) {
const char kV2StopSessionMessage[] = R"(
{
"type":"v2_message",
"message": {
"type":"STOP"
},
"sequenceNumber":-1,
"timeoutMillis":0,
"clientId":"152127444812943594"
})";
auto message = DialInternalMessage::From(kV2StopSessionMessage);
ASSERT_TRUE(message);
EXPECT_EQ(DialInternalMessageType::kV2Message, message->type);
EXPECT_EQ("152127444812943594", message->client_id);
EXPECT_EQ(-1, message->sequence_number);
EXPECT_TRUE(DialInternalMessageUtil::IsStopSessionMessage(*message));
}
TEST_F(DialInternalMessageUtilTest, CreateReceiverActionCastMessage) {
const char kReceiverActionCastMessage[] = R"(
{
"clientId":"152127444812943594",
"message": {
"action":"cast",
"receiver": {
"capabilities":[],
"displayStatus":null,
"friendlyName":"Lab Roku",
"ipAddress":"172.17.32.151",
"isActiveInput":null,
"label":"vSzzcOE6bD_NrLSbPN-qswEktGk",
"receiverType":"dial",
"volume":null
}
},
"sequenceNumber":-1,
"timeoutMillis":0,
"type":"receiver_action"
})";
content::PresentationConnectionMessage message =
DialInternalMessageUtil::CreateReceiverActionCastMessage(launch_info_,
sink_);
ASSERT_TRUE(message.message);
ExpectMessagesEqual(kReceiverActionCastMessage, *message.message);
}
TEST_F(DialInternalMessageUtilTest, CreateReceiverActionStopMessage) {
const char kReceiverActionStopMessage[] = R"(
{
"clientId":"152127444812943594",
"message": {
"action":"stop",
"receiver": {
"capabilities":[],
"displayStatus":null,
"friendlyName":"Lab Roku",
"ipAddress":"172.17.32.151",
"isActiveInput":null,
"label":"vSzzcOE6bD_NrLSbPN-qswEktGk",
"receiverType":"dial",
"volume":null
}
},
"sequenceNumber":-1,
"timeoutMillis":0,
"type":"receiver_action"
})";
content::PresentationConnectionMessage message =
DialInternalMessageUtil::CreateReceiverActionStopMessage(launch_info_,
sink_);
ASSERT_TRUE(message.message);
ExpectMessagesEqual(kReceiverActionStopMessage, *message.message);
}
TEST_F(DialInternalMessageUtilTest, CreateNewSessionMessage) {
const char kNewSessionMessage[] = R"(
{
"clientId":"152127444812943594",
"message": {
"appId":"",
"appImages":[],
"displayName":"YouTube",
"media":[],
"namespaces":[],
"receiver": {
"capabilities":[],
"displayStatus":null,
"friendlyName":"Lab Roku",
"ipAddress":"172.17.32.151",
"isActiveInput":null,
"label":"vSzzcOE6bD_NrLSbPN-qswEktGk",
"receiverType":"dial",
"volume":null
},
"senderApps":[],
"sessionId":"1",
"status":"connected",
"statusText":"",
"transportId":""
},
"sequenceNumber":-1,
"timeoutMillis":0,
"type":"new_session"
})";
content::PresentationConnectionMessage message =
DialInternalMessageUtil::CreateNewSessionMessage(launch_info_, sink_);
ASSERT_TRUE(message.message);
ExpectMessagesEqual(kNewSessionMessage, *message.message);
}
TEST_F(DialInternalMessageUtilTest, CreateCustomDialLaunchMessage) {
const char kCustomDialLaunchMessage[] = R"(
{
"clientId":"152127444812943594",
"message": {
"appState":"stopped",
"receiver": {
"capabilities":[],
"displayStatus":null,
"friendlyName":"Lab Roku",
"ipAddress":"172.17.32.151",
"isActiveInput":null,
"label":"vSzzcOE6bD_NrLSbPN-qswEktGk",
"receiverType":"dial",
"volume":null
}
},
"sequenceNumber":%d,
"timeoutMillis":0,
"type":"custom_dial_launch"
})";
ParsedDialAppInfo app_info =
CreateParsedDialAppInfo("YouTube", DialAppState::kStopped);
auto message_and_seq_num =
DialInternalMessageUtil::CreateCustomDialLaunchMessage(launch_info_,
sink_, app_info);
const auto& message = message_and_seq_num.first;
int seq_num = message_and_seq_num.second;
ASSERT_TRUE(message.message);
ExpectMessagesEqual(base::StringPrintf(kCustomDialLaunchMessage, seq_num),
*message.message);
}
} // namespace media_router
...@@ -101,6 +101,32 @@ void TestDialURLFetcher::StartDownload() { ...@@ -101,6 +101,32 @@ void TestDialURLFetcher::StartDownload() {
256 * 1024); 256 * 1024);
} }
TestDialActivityManager::TestDialActivityManager(
network::TestURLLoaderFactory* factory)
: DialActivityManager(), factory_(factory) {}
TestDialActivityManager::~TestDialActivityManager() = default;
std::unique_ptr<DialURLFetcher> TestDialActivityManager::CreateFetcher(
DialURLFetcher::SuccessCallback success_cb,
DialURLFetcher::ErrorCallback error_cb) {
OnFetcherCreated();
auto fetcher = std::make_unique<TestDialURLFetcher>(
std::move(success_cb), std::move(error_cb), factory_);
EXPECT_CALL(*fetcher, DoStart(expected_url_, expected_method_,
expected_post_data_, testing::_));
return fetcher;
}
void TestDialActivityManager::SetExpectedRequest(
const GURL& url,
const std::string& method,
const base::Optional<std::string>& post_data) {
EXPECT_CALL(*this, OnFetcherCreated());
expected_url_ = url;
expected_method_ = method;
expected_post_data_ = post_data;
}
net::IPEndPoint CreateIPEndPoint(int num) { net::IPEndPoint CreateIPEndPoint(int num) {
net::IPAddress ip_address; net::IPAddress ip_address;
CHECK(ip_address.AssignFromIPLiteral( CHECK(ip_address.AssignFromIPLiteral(
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
#include "chrome/browser/media/router/discovery/dial/dial_url_fetcher.h" #include "chrome/browser/media/router/discovery/dial/dial_url_fetcher.h"
#include "chrome/browser/media/router/discovery/mdns/cast_media_sink_service.h" #include "chrome/browser/media/router/discovery/mdns/cast_media_sink_service.h"
#include "chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.h" #include "chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.h"
#include "chrome/browser/media/router/providers/dial/dial_activity_manager.h"
#include "chrome/common/media_router/discovery/media_sink_internal.h" #include "chrome/common/media_router/discovery/media_sink_internal.h"
#include "net/base/ip_endpoint.h" #include "net/base/ip_endpoint.h"
#include "services/network/test/test_url_loader_factory.h" #include "services/network/test/test_url_loader_factory.h"
...@@ -178,6 +179,31 @@ class TestDialURLFetcher : public DialURLFetcher { ...@@ -178,6 +179,31 @@ class TestDialURLFetcher : public DialURLFetcher {
network::TestURLLoaderFactory* const factory_; network::TestURLLoaderFactory* const factory_;
}; };
class TestDialActivityManager : public DialActivityManager {
public:
explicit TestDialActivityManager(network::TestURLLoaderFactory* factory);
~TestDialActivityManager() override;
std::unique_ptr<DialURLFetcher> CreateFetcher(
DialURLFetcher::SuccessCallback success_cb,
DialURLFetcher::ErrorCallback error_cb) override;
void SetExpectedRequest(const GURL& url,
const std::string& method,
const base::Optional<std::string>& post_data);
MOCK_METHOD0(OnFetcherCreated, void());
private:
network::TestURLLoaderFactory* const factory_;
GURL expected_url_;
std::string expected_method_;
base::Optional<std::string> expected_post_data_;
DISALLOW_COPY_AND_ASSIGN(TestDialActivityManager);
};
// Helper function to create an IP endpoint object. // Helper function to create an IP endpoint object.
// If |num| is 1, returns 192.168.0.101:8009; // If |num| is 1, returns 192.168.0.101:8009;
// If |num| is 2, returns 192.168.0.102:8009. // If |num| is 2, returns 192.168.0.102:8009.
......
...@@ -3124,6 +3124,8 @@ test("unit_tests") { ...@@ -3124,6 +3124,8 @@ test("unit_tests") {
"../browser/media/router/providers/cast/cast_app_discovery_service_unittest.cc", "../browser/media/router/providers/cast/cast_app_discovery_service_unittest.cc",
"../browser/media/router/providers/cast/cast_media_route_provider_metrics_unittest.cc", "../browser/media/router/providers/cast/cast_media_route_provider_metrics_unittest.cc",
"../browser/media/router/providers/cast/dual_media_sink_service_unittest.cc", "../browser/media/router/providers/cast/dual_media_sink_service_unittest.cc",
"../browser/media/router/providers/dial/dial_activity_manager_unittest.cc",
"../browser/media/router/providers/dial/dial_internal_message_util_unittest.cc",
"../browser/media/router/providers/dial/dial_media_route_provider_unittest.cc", "../browser/media/router/providers/dial/dial_media_route_provider_unittest.cc",
"../browser/media/router/providers/extension/extension_media_route_provider_proxy_unittest.cc", "../browser/media/router/providers/extension/extension_media_route_provider_proxy_unittest.cc",
"../browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc", "../browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment