aboutsummaryrefslogtreecommitdiff
path: root/cast
diff options
context:
space:
mode:
authorYuri Wiitala <miu@chromium.org>2020-10-05 15:15:08 -0700
committerCommit Bot <commit-bot@chromium.org>2020-10-05 23:11:31 +0000
commiteee0513a73bc7ef3f913865f1b359ec6d29e4308 (patch)
tree728800c45c43f7129d7bf29435bf30b2af5ae0da /cast
parent952113a3bdf55baa055d74c216260edb3dce46a6 (diff)
downloadopenscreen-eee0513a73bc7ef3f913865f1b359ec6d29e4308.tar.gz
Cast Receiver Application Agent
Adds an agent that handles the Cast V2 Application Control messaging, and implements a Cast Application "switcher" and application message router. Added a MakeUniqueSessionId() utility to de-dupe functions that generate transport IDs for Cast Channel messaging. Bug: b/170134050 Change-Id: I1d79e7e3c479dd4f3dc35406b0e64046b4fa011b Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2449149 Commit-Queue: Yuri Wiitala <miu@chromium.org> Reviewed-by: Jordan Bayles <jophba@chromium.org>
Diffstat (limited to 'cast')
-rw-r--r--cast/common/channel/cast_socket_message_port.cc2
-rw-r--r--cast/common/channel/cast_socket_message_port.h2
-rw-r--r--cast/common/channel/message_util.cc11
-rw-r--r--cast/common/channel/message_util.h38
-rw-r--r--cast/common/channel/virtual_connection.h4
-rw-r--r--cast/common/public/cast_socket.h2
-rw-r--r--cast/receiver/BUILD.gn24
-rw-r--r--cast/receiver/DEPS6
-rw-r--r--cast/receiver/application_agent.cc402
-rw-r--r--cast/receiver/application_agent.h169
-rw-r--r--cast/receiver/application_agent_unittest.cc605
-rw-r--r--cast/sender/cast_platform_client.cc15
-rw-r--r--cast/streaming/sender_session.cc18
-rw-r--r--cast/streaming/sender_session.h3
14 files changed, 1263 insertions, 38 deletions
diff --git a/cast/common/channel/cast_socket_message_port.cc b/cast/common/channel/cast_socket_message_port.cc
index b6d65123..7b16f3e3 100644
--- a/cast/common/channel/cast_socket_message_port.cc
+++ b/cast/common/channel/cast_socket_message_port.cc
@@ -88,7 +88,7 @@ void CastSocketMessagePort::OnMessage(VirtualConnectionRouter* router,
CastSocket* socket,
::cast::channel::CastMessage message) {
OSP_DCHECK(router == router_);
- OSP_DCHECK(socket_.get() == socket);
+ OSP_DCHECK(!socket || socket_.get() == socket);
OSP_DVLOG << "Received a cast socket message";
if (!client_) {
OSP_DLOG_WARN << "Dropping message due to nullptr client_";
diff --git a/cast/common/channel/cast_socket_message_port.h b/cast/common/channel/cast_socket_message_port.h
index 4dbd141c..36b25263 100644
--- a/cast/common/channel/cast_socket_message_port.h
+++ b/cast/common/channel/cast_socket_message_port.h
@@ -24,6 +24,8 @@ class CastSocketMessagePort : public MessagePort, public CastMessageHandler {
explicit CastSocketMessagePort(VirtualConnectionRouter* router);
~CastSocketMessagePort() override;
+ const std::string& client_sender_id() const { return client_sender_id_; }
+
void SetSocket(WeakPtr<CastSocket> socket);
// Returns current socket identifier, or -1 if not connected.
diff --git a/cast/common/channel/message_util.cc b/cast/common/channel/message_util.cc
index 09609eea..79b942bd 100644
--- a/cast/common/channel/message_util.cc
+++ b/cast/common/channel/message_util.cc
@@ -4,6 +4,9 @@
#include "cast/common/channel/message_util.h"
+#include <sstream>
+#include <utility>
+
#include "util/osp_logging.h"
namespace openscreen {
@@ -67,5 +70,13 @@ CastMessage MakeCloseMessage(const std::string& source_id,
return close_message;
}
+std::string MakeUniqueSessionId(const char* prefix) {
+ static int next_id = 10000;
+
+ std::ostringstream oss;
+ oss << prefix << '-' << (next_id++);
+ return oss.str();
+}
+
} // namespace cast
} // namespace openscreen
diff --git a/cast/common/channel/message_util.h b/cast/common/channel/message_util.h
index f1ba2ead..fcb25651 100644
--- a/cast/common/channel/message_util.h
+++ b/cast/common/channel/message_util.h
@@ -5,6 +5,8 @@
#ifndef CAST_COMMON_CHANNEL_MESSAGE_UTIL_H_
#define CAST_COMMON_CHANNEL_MESSAGE_UTIL_H_
+#include <string>
+
#include "absl/strings/string_view.h"
#include "cast/common/channel/proto/cast_channel.pb.h"
@@ -48,7 +50,9 @@ static constexpr char kMessageKeyProtocolVersionList[] = "protocolVersionList";
static constexpr char kMessageKeyReasonCode[] = "reasonCode";
static constexpr char kMessageKeyAppId[] = "appId";
static constexpr char kMessageKeyRequestId[] = "requestId";
-static constexpr char kMessageKeyAvailability[] = "availability";
+static constexpr char kMessageKeyResponseType[] = "responseType";
+static constexpr char kMessageKeyTransportId[] = "transportId";
+static constexpr char kMessageKeySessionId[] = "sessionId";
// JSON message field values.
static constexpr char kMessageTypeConnect[] = "CONNECT";
@@ -57,6 +61,33 @@ static constexpr char kMessageTypeConnected[] = "CONNECTED";
static constexpr char kMessageValueAppAvailable[] = "APP_AVAILABLE";
static constexpr char kMessageValueAppUnavailable[] = "APP_UNAVAILABLE";
+// JSON message key strings specific to application control messages.
+static constexpr char kMessageKeyAvailability[] = "availability";
+static constexpr char kMessageKeyAppParams[] = "appParams";
+static constexpr char kMessageKeyApplications[] = "applications";
+static constexpr char kMessageKeyControlType[] = "controlType";
+static constexpr char kMessageKeyDisplayName[] = "displayName";
+static constexpr char kMessageKeyIsIdleScreen[] = "isIdleScreen";
+static constexpr char kMessageKeyLaunchedFromCloud[] = "launchedFromCloud";
+static constexpr char kMessageKeyLevel[] = "level";
+static constexpr char kMessageKeyMuted[] = "muted";
+static constexpr char kMessageKeyName[] = "name";
+static constexpr char kMessageKeyNamespaces[] = "namespaces";
+static constexpr char kMessageKeyReason[] = "reason";
+static constexpr char kMessageKeyStatus[] = "status";
+static constexpr char kMessageKeyStepInterval[] = "stepInterval";
+static constexpr char kMessageKeyUniversalAppId[] = "universalAppId";
+static constexpr char kMessageKeyUserEq[] = "userEq";
+static constexpr char kMessageKeyVolume[] = "volume";
+
+// JSON message field value strings specific to application control messages.
+static constexpr char kMessageValueAttenuation[] = "attenuation";
+static constexpr char kMessageValueBadParameter[] = "BAD_PARAMETER";
+static constexpr char kMessageValueInvalidSessionId[] = "INVALID_SESSION_ID";
+static constexpr char kMessageValueInvalidCommand[] = "INVALID_COMMAND";
+static constexpr char kMessageValueNotFound[] = "NOT_FOUND";
+static constexpr char kMessageValueSystemError[] = "SYSTEM_ERROR";
+
// TODO(crbug.com/openscreen/111): Add validation that each message type is
// received on the correct namespace. This will probably involve creating a
// data structure for mapping between type and namespace.
@@ -198,6 +229,11 @@ inline bool IsTransportNamespace(absl::string_view namespace_) {
const std::string& source_id,
const std::string& destination_id);
+// Returns a session/transport ID string that is unique within this application
+// instance, having the format "prefix-12345". For example, calling this with a
+// |prefix| of "sender" will result in a string like "sender-12345".
+std::string MakeUniqueSessionId(const char* prefix);
+
} // namespace cast
} // namespace openscreen
diff --git a/cast/common/channel/virtual_connection.h b/cast/common/channel/virtual_connection.h
index 6f8b2cb8..5e94c372 100644
--- a/cast/common/channel/virtual_connection.h
+++ b/cast/common/channel/virtual_connection.h
@@ -92,9 +92,9 @@ struct VirtualConnection {
// - sender-0 or receiver-0: identifies the appropriate platform endpoint of
// the device. Authentication and transport-related messages use these.
// - sender-12345: Possible form of a Cast sender ID. The number portion is
- // randomly generated and intended to be unique within that device.
+ // intended to be unique within that device (i.e., unique per CastSocket).
// - Random decimal number: Possible form of a Cast sender ID. Also randomly
- // generated and intended to be unique within that device.
+ // intended to be unique within that device (i.e., unique per CastSocket).
// - GUID-style hex string: Random string identifying a particular receiver
// app on the device.
//
diff --git a/cast/common/public/cast_socket.h b/cast/common/public/cast_socket.h
index 2a67b659..5c0b8775 100644
--- a/cast/common/public/cast_socket.h
+++ b/cast/common/public/cast_socket.h
@@ -80,7 +80,7 @@ class CastSocket : public TlsConnection::Client {
};
// Returns socket->socket_id() if |socket| is not null, otherwise 0.
-inline int ToCastSocketId(CastSocket* socket) {
+constexpr int ToCastSocketId(CastSocket* socket) {
return socket ? socket->socket_id() : 0;
}
diff --git a/cast/receiver/BUILD.gn b/cast/receiver/BUILD.gn
index b9a65734..099a178a 100644
--- a/cast/receiver/BUILD.gn
+++ b/cast/receiver/BUILD.gn
@@ -28,6 +28,24 @@ source_set("channel") {
]
}
+source_set("agent") {
+ sources = [
+ "application_agent.cc",
+ "application_agent.h",
+ ]
+
+ public_deps = [
+ "../../platform",
+ "../common:channel",
+ "../common:public",
+ ]
+
+ deps = [
+ ":channel",
+ "../../util",
+ ]
+}
+
source_set("test_helpers") {
testonly = true
sources = [
@@ -48,9 +66,13 @@ source_set("test_helpers") {
source_set("unittests") {
testonly = true
- sources = [ "channel/device_auth_namespace_handler_unittest.cc" ]
+ sources = [
+ "application_agent_unittest.cc",
+ "channel/device_auth_namespace_handler_unittest.cc",
+ ]
deps = [
+ ":agent",
":channel",
":test_helpers",
"../../platform:test",
diff --git a/cast/receiver/DEPS b/cast/receiver/DEPS
index a2def1b0..05ea4e83 100644
--- a/cast/receiver/DEPS
+++ b/cast/receiver/DEPS
@@ -5,3 +5,9 @@ include_rules = [
'+cast/common',
'+cast/receiver'
]
+
+specific_include_rules = {
+ 'application_agent_unittest.cc': [
+ '+json/writer.h',
+ ],
+}
diff --git a/cast/receiver/application_agent.cc b/cast/receiver/application_agent.cc
new file mode 100644
index 00000000..4dc435d6
--- /dev/null
+++ b/cast/receiver/application_agent.cc
@@ -0,0 +1,402 @@
+// Copyright 2020 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 "cast/receiver/application_agent.h"
+
+#include <utility>
+
+#include "cast/common/channel/message_util.h"
+#include "cast/common/channel/virtual_connection.h"
+#include "cast/common/public/cast_socket.h"
+#include "platform/base/tls_credentials.h"
+#include "platform/base/tls_listen_options.h"
+#include "util/json/json_serialization.h"
+#include "util/osp_logging.h"
+
+namespace openscreen {
+namespace cast {
+namespace {
+
+// Parses the given string as a JSON object. If the parse fails, an empty object
+// is returned.
+Json::Value ParseAsObject(absl::string_view value) {
+ ErrorOr<Json::Value> parsed = json::Parse(value);
+ if (parsed.is_value() && parsed.value().isObject()) {
+ return std::move(parsed.value());
+ }
+ return Json::Value(Json::objectValue);
+}
+
+// Returns true if the type field in |object| is set to the given |type|.
+bool HasType(const Json::Value& object, CastMessageType type) {
+ OSP_DCHECK(object.isObject());
+ const Json::Value& value =
+ object.get(kMessageKeyType, Json::Value::nullSingleton());
+ return value.isString() && value.asString() == CastMessageTypeToString(type);
+}
+
+// Returns the first app ID for the given |app|, or the empty string if there is
+// none.
+std::string GetFirstAppId(ApplicationAgent::Application* app) {
+ const auto& app_ids = app->GetAppIds();
+ return app_ids.empty() ? std::string() : app_ids.front();
+}
+
+} // namespace
+
+ApplicationAgent::ApplicationAgent(
+ TaskRunner* task_runner,
+ DeviceAuthNamespaceHandler::CredentialsProvider* credentials_provider)
+ : task_runner_(task_runner),
+ auth_handler_(credentials_provider),
+ connection_handler_(&connection_manager_, this),
+ router_(&connection_manager_),
+ message_port_(&router_) {
+ router_.AddHandlerForLocalId(kPlatformReceiverId, this);
+}
+
+ApplicationAgent::~ApplicationAgent() {
+ OSP_DCHECK(task_runner_->IsRunningOnTaskRunner());
+
+ idle_screen_app_ = nullptr; // Prevent re-launching the idle screen app.
+ SwitchToApplication({}, {}, nullptr);
+
+ router_.RemoveHandlerForLocalId(kPlatformReceiverId);
+}
+
+void ApplicationAgent::RegisterApplication(Application* app,
+ bool auto_launch_for_idle_screen) {
+ OSP_DCHECK(app);
+
+ for (const std::string& app_id : app->GetAppIds()) {
+ OSP_DCHECK(!app_id.empty());
+ const auto insert_result = registered_applications_.insert({app_id, app});
+ // The insert must not fail (prior entry for same key).
+ OSP_DCHECK(insert_result.second);
+ }
+
+ if (auto_launch_for_idle_screen) {
+ OSP_DCHECK(!idle_screen_app_);
+ idle_screen_app_ = app;
+ // Launch the idle screen app, if no app was running.
+ if (!launched_app_) {
+ GoIdle();
+ }
+ }
+}
+
+void ApplicationAgent::UnregisterApplication(Application* app) {
+ for (auto it = registered_applications_.begin();
+ it != registered_applications_.end();) {
+ if (it->second == app) {
+ it = registered_applications_.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ if (idle_screen_app_ == app) {
+ idle_screen_app_ = nullptr;
+ }
+
+ if (launched_app_ == app) {
+ GoIdle();
+ }
+}
+
+void ApplicationAgent::StopApplicationIfRunning(Application* app) {
+ if (launched_app_ == app) {
+ GoIdle();
+ }
+}
+
+void ApplicationAgent::OnConnected(ReceiverSocketFactory* factory,
+ const IPEndpoint& endpoint,
+ std::unique_ptr<CastSocket> socket) {
+ router_.TakeSocket(this, std::move(socket));
+}
+
+void ApplicationAgent::OnError(ReceiverSocketFactory* factory, Error error) {
+ OSP_LOG_ERROR << "Cast agent received socket factory error: " << error;
+}
+
+void ApplicationAgent::OnMessage(VirtualConnectionRouter* router,
+ CastSocket* socket,
+ ::cast::channel::CastMessage message) {
+ if (message_port_.GetSocketId() == ToCastSocketId(socket) &&
+ !message_port_.client_sender_id().empty() &&
+ message_port_.client_sender_id() == message.destination_id()) {
+ message_port_.OnMessage(router, socket, std::move(message));
+ return;
+ }
+
+ if (message.destination_id() != kPlatformReceiverId &&
+ message.destination_id() != kBroadcastId) {
+ return; // Message not for us.
+ }
+
+ const std::string& ns = message.namespace_();
+ if (ns == kConnectionNamespace) {
+ connection_handler_.OnMessage(router, socket, std::move(message));
+ return;
+ }
+ if (ns == kAuthNamespace) {
+ auth_handler_.OnMessage(router, socket, std::move(message));
+ return;
+ }
+
+ const Json::Value request = ParseAsObject(message.payload_utf8());
+ Json::Value response;
+ if (ns == kHeartbeatNamespace) {
+ if (HasType(request, CastMessageType::kPing)) {
+ response = HandlePing();
+ }
+ } else if (ns == kReceiverNamespace) {
+ if (request[kMessageKeyRequestId].isNull()) {
+ response = HandleInvalidCommand(request);
+ } else if (HasType(request, CastMessageType::kGetAppAvailability)) {
+ response = HandleGetAppAvailability(request);
+ } else if (HasType(request, CastMessageType::kGetStatus)) {
+ response = HandleGetStatus(request);
+ } else if (HasType(request, CastMessageType::kLaunch)) {
+ response = HandleLaunch(request, socket);
+ } else if (HasType(request, CastMessageType::kStop)) {
+ response = HandleStop(request);
+ } else {
+ response = HandleInvalidCommand(request);
+ }
+ } else {
+ // Ignore messages for all other namespaces.
+ }
+
+ if (!response.empty()) {
+ router_.Send(VirtualConnection{message.destination_id(),
+ message.source_id(), ToCastSocketId(socket)},
+ MakeSimpleUTF8Message(ns, json::Stringify(response).value()));
+ }
+}
+
+bool ApplicationAgent::IsConnectionAllowed(
+ const VirtualConnection& virtual_conn) const {
+ return true;
+}
+
+void ApplicationAgent::OnClose(CastSocket* socket) {
+ if (message_port_.GetSocketId() == ToCastSocketId(socket)) {
+ OSP_VLOG << "Cast agent socket closed.";
+ GoIdle();
+ }
+}
+
+void ApplicationAgent::OnError(CastSocket* socket, Error error) {
+ if (message_port_.GetSocketId() == ToCastSocketId(socket)) {
+ OSP_LOG_ERROR << "Cast agent received socket error: " << error;
+ GoIdle();
+ }
+}
+
+Json::Value ApplicationAgent::HandlePing() {
+ Json::Value response;
+ response[kMessageKeyType] = CastMessageTypeToString(CastMessageType::kPong);
+ return response;
+}
+
+Json::Value ApplicationAgent::HandleGetAppAvailability(
+ const Json::Value& request) {
+ Json::Value response;
+ const Json::Value& app_ids = request[kMessageKeyAppId];
+ if (app_ids.isArray()) {
+ response[kMessageKeyRequestId] = request[kMessageKeyRequestId];
+ response[kMessageKeyResponseType] = request[kMessageKeyType];
+ Json::Value& availability = response[kMessageKeyAvailability];
+ for (const Json::Value& app_id : app_ids) {
+ if (app_id.isString()) {
+ const auto app_id_str = app_id.asString();
+ availability[app_id_str] = registered_applications_.count(app_id_str)
+ ? kMessageValueAppAvailable
+ : kMessageValueAppUnavailable;
+ }
+ }
+ }
+ return response;
+}
+
+Json::Value ApplicationAgent::HandleGetStatus(const Json::Value& request) {
+ Json::Value response;
+ PopulateReceiverStatus(&response);
+ response[kMessageKeyRequestId] = request[kMessageKeyRequestId];
+ return response;
+}
+
+Json::Value ApplicationAgent::HandleLaunch(const Json::Value& request,
+ CastSocket* socket) {
+ const Json::Value& app_id = request[kMessageKeyAppId];
+ Error error;
+ if (app_id.isString() && !app_id.asString().empty()) {
+ error = SwitchToApplication(app_id.asString(),
+ request[kMessageKeyAppParams], socket);
+ } else {
+ error = Error(Error::Code::kParameterInvalid, kMessageValueBadParameter);
+ }
+ if (!error.ok()) {
+ Json::Value response;
+ response[kMessageKeyRequestId] = request[kMessageKeyRequestId];
+ response[kMessageKeyType] =
+ CastMessageTypeToString(CastMessageType::kLaunchError);
+ response[kMessageKeyReason] = error.message();
+ return response;
+ }
+
+ // Note: No reply is sent. Instead, the requestor will get a RECEIVER_STATUS
+ // broadcast message from SwitchToApplication(), which is how it will see that
+ // the launch succeeded.
+ return {};
+}
+
+Json::Value ApplicationAgent::HandleStop(const Json::Value& request) {
+ const Json::Value& session_id = request[kMessageKeySessionId];
+ if (session_id.isNull()) {
+ GoIdle();
+ return {};
+ }
+
+ if (session_id.isString() && launched_app_ &&
+ session_id.asString() == launched_app_->GetSessionId()) {
+ GoIdle();
+ return {};
+ }
+
+ Json::Value response;
+ response[kMessageKeyRequestId] = request[kMessageKeyRequestId];
+ response[kMessageKeyType] =
+ CastMessageTypeToString(CastMessageType::kInvalidRequest);
+ response[kMessageKeyReason] = kMessageValueInvalidSessionId;
+ return response;
+}
+
+Json::Value ApplicationAgent::HandleInvalidCommand(const Json::Value& request) {
+ Json::Value response;
+ if (request[kMessageKeyRequestId].isNull()) {
+ return response;
+ }
+ response[kMessageKeyRequestId] = request[kMessageKeyRequestId];
+ response[kMessageKeyType] =
+ CastMessageTypeToString(CastMessageType::kInvalidRequest);
+ response[kMessageKeyReason] = kMessageValueInvalidCommand;
+ return response;
+}
+
+Error ApplicationAgent::SwitchToApplication(std::string app_id,
+ const Json::Value& app_params,
+ CastSocket* socket) {
+ Error error = Error::Code::kNone;
+ Application* desired_app = nullptr;
+ Application* fallback_app = nullptr;
+ if (!app_id.empty()) {
+ const auto it = registered_applications_.find(app_id);
+ if (it != registered_applications_.end()) {
+ desired_app = it->second;
+ if (desired_app != idle_screen_app_) {
+ fallback_app = idle_screen_app_;
+ }
+ } else {
+ return Error(Error::Code::kItemNotFound, kMessageValueNotFound);
+ }
+ }
+
+ if (launched_app_ == desired_app) {
+ return error;
+ }
+
+ if (launched_app_) {
+ launched_app_->Stop();
+ message_port_.SetSocket({});
+ launched_app_ = nullptr;
+ launched_via_app_id_ = {};
+ }
+
+ if (desired_app) {
+ if (socket) {
+ message_port_.SetSocket(socket->GetWeakPtr());
+ }
+ if (desired_app->Launch(app_id, app_params, &message_port_)) {
+ launched_app_ = desired_app;
+ launched_via_app_id_ = std::move(app_id);
+ } else {
+ error = Error(Error::Code::kUnknownError, kMessageValueSystemError);
+ message_port_.SetSocket({});
+ }
+ }
+
+ if (!launched_app_ && fallback_app) {
+ app_id = GetFirstAppId(fallback_app);
+ if (fallback_app->Launch(app_id, {}, &message_port_)) {
+ launched_app_ = fallback_app;
+ launched_via_app_id_ = std::move(app_id);
+ }
+ }
+
+ BroadcastReceiverStatus();
+
+ return error;
+}
+
+void ApplicationAgent::GoIdle() {
+ std::string app_id;
+ if (idle_screen_app_) {
+ app_id = GetFirstAppId(idle_screen_app_);
+ }
+ SwitchToApplication(app_id, {}, nullptr);
+}
+
+void ApplicationAgent::PopulateReceiverStatus(Json::Value* out) {
+ Json::Value& message = *out;
+ message[kMessageKeyType] =
+ CastMessageTypeToString(CastMessageType::kReceiverStatus);
+ Json::Value& status = message[kMessageKeyStatus];
+
+ if (launched_app_) {
+ Json::Value& details = status[kMessageKeyApplications][0];
+ if (!message_port_.client_sender_id().empty()) {
+ details[kMessageKeyTransportId] = message_port_.client_sender_id();
+ }
+ details[kMessageKeySessionId] = launched_app_->GetSessionId();
+ details[kMessageKeyAppId] = launched_via_app_id_;
+ details[kMessageKeyUniversalAppId] = launched_via_app_id_;
+ details[kMessageKeyDisplayName] = launched_app_->GetDisplayName();
+ details[kMessageKeyIsIdleScreen] = (launched_app_ == idle_screen_app_);
+ details[kMessageKeyLaunchedFromCloud] = false;
+ std::vector<std::string> app_namespaces =
+ launched_app_->GetSupportedNamespaces();
+ Json::Value& namespaces =
+ (details[kMessageKeyNamespaces] = Json::Value(Json::arrayValue));
+ for (int i = 0, count = app_namespaces.size(); i < count; ++i) {
+ namespaces[i][kMessageKeyName] = std::move(app_namespaces[i]);
+ }
+ }
+
+ status[kMessageKeyUserEq] = Json::Value(Json::objectValue);
+
+ // Indicate a fixed 100% volume level.
+ Json::Value& volume = status[kMessageKeyVolume];
+ volume[kMessageKeyControlType] = kMessageValueAttenuation;
+ volume[kMessageKeyLevel] = 1.0;
+ volume[kMessageKeyMuted] = false;
+ volume[kMessageKeyStepInterval] = 0.05;
+}
+
+void ApplicationAgent::BroadcastReceiverStatus() {
+ Json::Value message;
+ PopulateReceiverStatus(&message);
+ message[kMessageKeyRequestId] = Json::Value(0); // Indicates no requestor.
+ router_.BroadcastFromLocalPeer(
+ kPlatformReceiverId,
+ MakeSimpleUTF8Message(kReceiverNamespace,
+ json::Stringify(message).value()));
+}
+
+ApplicationAgent::Application::~Application() = default;
+
+} // namespace cast
+} // namespace openscreen
diff --git a/cast/receiver/application_agent.h b/cast/receiver/application_agent.h
new file mode 100644
index 00000000..4aa551d3
--- /dev/null
+++ b/cast/receiver/application_agent.h
@@ -0,0 +1,169 @@
+// Copyright 2020 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 CAST_RECEIVER_APPLICATION_AGENT_H_
+#define CAST_RECEIVER_APPLICATION_AGENT_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "cast/common/channel/cast_socket_message_port.h"
+#include "cast/common/channel/connection_namespace_handler.h"
+#include "cast/common/channel/virtual_connection_manager.h"
+#include "cast/common/channel/virtual_connection_router.h"
+#include "cast/receiver/channel/device_auth_namespace_handler.h"
+#include "cast/receiver/public/receiver_socket_factory.h"
+#include "platform/api/serial_delete_ptr.h"
+#include "platform/api/task_runner.h"
+#include "platform/base/error.h"
+#include "platform/base/ip_address.h"
+#include "util/json/json_value.h"
+
+namespace openscreen {
+namespace cast {
+
+class CastSocket;
+
+// A service accepting CastSocket connections, and providing a minimal
+// implementation of the CastV2 application control protocol to launch receiver
+// applications and route messages to/from them.
+//
+// Workflow: One or more Applications are registered under this ApplicationAgent
+// (e.g., a "mirroring" app). Later, a ReceiverSocketFactory (external to this
+// class) will listen and establish CastSocket connections, and then pass
+// CastSockets to this ApplicationAgent via the OnConnect() method. As each
+// connection is made, device authentication will take place. Then, Cast V2
+// application messages asking about application availability are received and
+// responded to, based on what Applications are registered. Finally, the remote
+// may request the LAUNCH of an Application (and later a STOP).
+//
+// In the meantime, this ApplicationAgent broadcasts RECEIVER_STATUS about what
+// application is running. In addition, it attempts to launch an "idle screen"
+// Application whenever no other Application is running. The "idle screen"
+// Application is usually some kind of screen saver or wallpaper/clock display.
+// Registering the "idle screen" Application is optional, and if it's not
+// registered, then nothing will be running during idle periods.
+class ApplicationAgent final
+ : public ReceiverSocketFactory::Client,
+ public CastMessageHandler,
+ public ConnectionNamespaceHandler::VirtualConnectionPolicy,
+ public VirtualConnectionRouter::SocketErrorHandler {
+ public:
+ class Application {
+ public:
+ // Returns the one or more application IDs that are supported. This list
+ // must not mutate while the Application is registered.
+ virtual const std::vector<std::string>& GetAppIds() const = 0;
+
+ // Launches the application and returns true if successful. |app_id| is the
+ // specific ID that was used to launch the app, and |app_params| is a
+ // pass-through for any arbitrary app-specfic structure (or null if not
+ // provided). If the Application wishes to send/receive messages, it uses
+ // the provided |message_port| and must call MessagePort::SetClient() before
+ // any flow will occur.
+ virtual bool Launch(const std::string& app_id,
+ const Json::Value& app_params,
+ MessagePort* message_port) = 0;
+
+ // These reflect the current state of the application, and the data is used
+ // to populate RECEIVER_STATUS messages.
+ virtual std::string GetSessionId() = 0;
+ virtual std::string GetDisplayName() = 0;
+ virtual std::vector<std::string> GetSupportedNamespaces() = 0;
+
+ // Stops the application, if running.
+ virtual void Stop() = 0;
+
+ protected:
+ virtual ~Application();
+ };
+
+ ApplicationAgent(
+ TaskRunner* task_runner,
+ DeviceAuthNamespaceHandler::CredentialsProvider* credentials_provider);
+
+ ~ApplicationAgent() final;
+
+ // Registers an Application for launching by this agent. |app| must outlive
+ // this ApplicationAgent, or until UnregisterApplication() is called.
+ void RegisterApplication(Application* app,
+ bool auto_launch_for_idle_screen = false);
+ void UnregisterApplication(Application* app);
+
+ // Stops the given |app| if it is the one currently running. This is used by
+ // applications that encounter "exit" conditions where they need to STOP
+ // (e.g., due to timeout of user activity, end of media playback, or fatal
+ // errors).
+ void StopApplicationIfRunning(Application* app);
+
+ private:
+ // ReceiverSocketFactory::Client overrides.
+ void OnConnected(ReceiverSocketFactory* factory,
+ const IPEndpoint& endpoint,
+ std::unique_ptr<CastSocket> socket) final;
+ void OnError(ReceiverSocketFactory* factory, Error error) final;
+
+ // CastMessageHandler overrides.
+ void OnMessage(VirtualConnectionRouter* router,
+ CastSocket* socket,
+ ::cast::channel::CastMessage message) final;
+
+ // ConnectionNamespaceHandler::VirtualConnectionPolicy overrides.
+ bool IsConnectionAllowed(const VirtualConnection& virtual_conn) const final;
+
+ // VirtualConnectionRouter::SocketErrorHandler overrides.
+ void OnClose(CastSocket* socket) final;
+ void OnError(CastSocket* socket, Error error) final;
+
+ // OnMessage() delegates to these to take action for each |request|. Each of
+ // these returns a non-empty response message if a reply should be sent back
+ // to the requestor.
+ Json::Value HandlePing();
+ Json::Value HandleGetAppAvailability(const Json::Value& request);
+ Json::Value HandleGetStatus(const Json::Value& request);
+ Json::Value HandleLaunch(const Json::Value& request, CastSocket* socket);
+ Json::Value HandleStop(const Json::Value& request);
+ Json::Value HandleInvalidCommand(const Json::Value& request);
+
+ // Stops the currently-running Application and attempts to launch the
+ // Application referred to by |app_id|. If this fails, the "idle screen"
+ // Application will be automatically launched as a failure fall-back. |socket|
+ // is non-null only when the application switch was caused by a remote LAUNCH
+ // request.
+ Error SwitchToApplication(std::string app_id,
+ const Json::Value& app_params,
+ CastSocket* socket);
+
+ // Stops the currently-running Application and launches the "idle screen."
+ void GoIdle();
+
+ // Populates the given |message| object with the RECEIVER_STATUS fields,
+ // reflecting the currently-launched app (if any), and a fake volume level
+ // status.
+ void PopulateReceiverStatus(Json::Value* message);
+
+ // Broadcasts new RECEIVER_STATUS to all endpoints. This is called after an
+ // Application LAUNCH or STOP.
+ void BroadcastReceiverStatus();
+
+ TaskRunner* const task_runner_;
+ DeviceAuthNamespaceHandler auth_handler_;
+ ConnectionNamespaceHandler connection_handler_;
+ VirtualConnectionManager connection_manager_;
+ VirtualConnectionRouter router_;
+
+ std::map<std::string, Application*> registered_applications_;
+ Application* idle_screen_app_ = nullptr;
+
+ CastSocketMessagePort message_port_;
+ Application* launched_app_ = nullptr;
+ std::string launched_via_app_id_;
+};
+
+} // namespace cast
+} // namespace openscreen
+
+#endif // CAST_RECEIVER_APPLICATION_AGENT_H_
diff --git a/cast/receiver/application_agent_unittest.cc b/cast/receiver/application_agent_unittest.cc
new file mode 100644
index 00000000..18191e71
--- /dev/null
+++ b/cast/receiver/application_agent_unittest.cc
@@ -0,0 +1,605 @@
+// Copyright 2020 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 "cast/receiver/application_agent.h"
+
+#include <iomanip>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "cast/common/channel/message_util.h"
+#include "cast/common/channel/testing/fake_cast_socket.h"
+#include "cast/common/public/message_port.h"
+#include "cast/receiver/channel/static_credentials.h"
+#include "cast/receiver/channel/testing/device_auth_test_helpers.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "json/writer.h" // Included to teach gtest how to pretty-print.
+#include "platform/api/time.h"
+#include "platform/test/fake_task_runner.h"
+#include "platform/test/paths.h"
+#include "testing/util/read_file.h"
+#include "util/json/json_serialization.h"
+
+namespace openscreen {
+namespace cast {
+namespace {
+
+using ::cast::channel::CastMessage;
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::Ne;
+using ::testing::NotNull;
+using ::testing::Sequence;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+
+// Returns the location of certificate and auth challenge data files for cast
+// receiver tests.
+std::string GetTestDataSubdir() {
+ return GetTestDataPath() + "cast/receiver/channel";
+}
+
+class TestCredentialsProvider final
+ : public DeviceAuthNamespaceHandler::CredentialsProvider {
+ public:
+ TestCredentialsProvider() {
+ const std::string dir = GetTestDataSubdir();
+ bssl::UniquePtr<X509> parsed_cert;
+ TrustStore fake_trust_store;
+ InitStaticCredentialsFromFiles(
+ &creds_, &parsed_cert, &fake_trust_store, dir + "/device_key.pem",
+ dir + "/device_chain.pem", dir + "/device_tls.pem");
+ }
+
+ absl::Span<const uint8_t> GetCurrentTlsCertAsDer() final {
+ return absl::Span<uint8_t>(creds_.tls_cert_der);
+ }
+ const DeviceCredentials& GetCurrentDeviceCredentials() final {
+ return creds_.device_creds;
+ }
+
+ private:
+ StaticCredentialsProvider creds_;
+};
+
+class TestAuthChallengeMessage : public CastMessage {
+ public:
+ TestAuthChallengeMessage() {
+ const auto result = ParseFromString(
+ ReadEntireFileToString(GetTestDataSubdir() + "/auth_challenge.pb"));
+ OSP_CHECK(result);
+ }
+};
+
+class FakeApplication : public ApplicationAgent::Application,
+ public MessagePort::Client {
+ public:
+ explicit FakeApplication(const char* app_id, const char* display_name)
+ : app_ids_({app_id}), display_name_(display_name) {
+ OSP_CHECK(app_ids_.front().size() == 8);
+ }
+
+ // These are called at the end of the Launch() and Stop() methods for
+ // confirming those methods were called.
+ MOCK_METHOD(void, DidLaunch, (Json::Value params, MessagePort* port), ());
+ MOCK_METHOD(void, DidStop, (), ());
+
+ // MessagePort::Client overrides.
+ MOCK_METHOD(void,
+ OnMessage,
+ (const std::string& source_sender_id,
+ const std::string& message_namespace,
+ const std::string& message),
+ (override));
+ MOCK_METHOD(void, OnError, (Error error), (override));
+
+ const std::vector<std::string>& GetAppIds() const override {
+ return app_ids_;
+ }
+
+ bool Launch(const std::string& app_id,
+ const Json::Value& app_params,
+ MessagePort* message_port) override {
+ EXPECT_EQ(GetAppIds().front(), app_id);
+ EXPECT_TRUE(message_port);
+ EXPECT_FALSE(is_launched_);
+ ++session_id_;
+ is_launched_ = true;
+ DidLaunch(app_params, message_port);
+ return true;
+ }
+
+ std::string GetSessionId() override {
+ std::ostringstream oss;
+ if (is_launched_) {
+ oss << GetAppIds().front() << "-9ABC-DEF0-1234-";
+ oss << std::setfill('0') << std::hex << std::setw(12) << session_id_;
+ }
+ return oss.str();
+ }
+
+ std::string GetDisplayName() override { return display_name_; }
+
+ std::vector<std::string> GetSupportedNamespaces() override {
+ return namespaces_;
+ }
+ void SetSupportedNamespaces(std::vector<std::string> the_namespaces) {
+ namespaces_ = std::move(the_namespaces);
+ }
+
+ void Stop() override {
+ EXPECT_TRUE(is_launched_);
+ is_launched_ = false;
+ DidStop();
+ }
+
+ private:
+ const std::vector<std::string> app_ids_;
+ const std::string display_name_;
+
+ std::vector<std::string> namespaces_;
+
+ int session_id_ = 0;
+ bool is_launched_ = false;
+};
+
+class ApplicationAgentTest : public ::testing::Test {
+ public:
+ ApplicationAgentTest() {
+ EXPECT_CALL(idle_app_, DidLaunch(_, NotNull()));
+ agent_.RegisterApplication(&idle_app_, true);
+ Mock::VerifyAndClearExpectations(&idle_app_);
+
+ ConnectAndDoAuth();
+ }
+
+ ~ApplicationAgentTest() override { EXPECT_CALL(idle_app_, DidStop()); }
+
+ ApplicationAgent* agent() { return &agent_; }
+ StrictMock<FakeApplication>* idle_app() { return &idle_app_; }
+
+ MockCastSocketClient* sender_inbound() {
+ return &socket_pair_.mock_peer_client;
+ }
+ CastSocket* sender_outbound() { return socket_pair_.peer_socket.get(); }
+
+ // Examines the |message| for the correct source/destination transport IDs and
+ // namespace, confirms there is JSON in the payload, and returns parsed JSON
+ // (or an empty object if the parse fails).
+ static Json::Value ValidateAndParseMessage(const CastMessage& message,
+ const std::string& from,
+ const std::string& to,
+ const std::string& the_namespace) {
+ EXPECT_EQ(from, message.source_id());
+ EXPECT_EQ(to, message.destination_id());
+ EXPECT_EQ(the_namespace, message.namespace_());
+ EXPECT_EQ(::cast::channel::CastMessage_PayloadType_STRING,
+ message.payload_type());
+ EXPECT_FALSE(message.payload_utf8().empty());
+ ErrorOr<Json::Value> parsed = json::Parse(message.payload_utf8());
+ return parsed.value(Json::Value(Json::objectValue));
+ }
+
+ // Constructs a CastMessage proto for sending via the CastSocket::Send() API.
+ static CastMessage MakeCastMessage(const std::string& source_id,
+ const std::string& destination_id,
+ const std::string& the_namespace,
+ const std::string& json) {
+ CastMessage message = MakeSimpleUTF8Message(the_namespace, json);
+ message.set_source_id(source_id);
+ message.set_destination_id(destination_id);
+ return message;
+ }
+
+ private:
+ // Walk through all the steps to establish a network connection to the
+ // ApplicationAgent, and test the plumbing for the auth challenge/reply.
+ void ConnectAndDoAuth() {
+ static_cast<ReceiverSocketFactory::Client*>(&agent_)->OnConnected(
+ nullptr, socket_pair_.local_endpoint, std::move(socket_pair_.socket));
+
+ // The remote will send the auth challenge message and get a reply.
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([](CastSocket*, CastMessage message) {
+ EXPECT_EQ(kAuthNamespace, message.namespace_());
+ EXPECT_FALSE(message.payload_binary().empty());
+ }));
+ const auto result = sender_outbound()->Send(TestAuthChallengeMessage());
+ ASSERT_TRUE(result.ok()) << result;
+ Mock::VerifyAndClearExpectations(sender_inbound());
+ }
+
+ void TearDown() override {
+ // The ApplicationAgent should send a final "no apps running"
+ // RECEIVER_STATUS broadcast to the sender at destruction time.
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":0,
+ "type":"RECEIVER_STATUS",
+ "status":{
+ "userEq":{},
+ "volume":{
+ "controlType":"attenuation",
+ "level":1.0,
+ "muted":false,
+ "stepInterval":0.05
+ }
+ }
+ })";
+ const Json::Value payload = ValidateAndParseMessage(
+ message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ }
+
+ FakeClock clock_{Clock::time_point() + std::chrono::hours(1)};
+ FakeTaskRunner task_runner_{&clock_};
+ FakeCastSocketPair socket_pair_;
+ StrictMock<FakeApplication> idle_app_{"E8C28D3C", "Backdrop"};
+ TestCredentialsProvider creds_;
+ ApplicationAgent agent_{&task_runner_, &creds_};
+};
+
+TEST_F(ApplicationAgentTest, JustConnectsWithoutDoingAnything) {}
+
+TEST_F(ApplicationAgentTest, IgnoresGarbageMessages) {
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _)).Times(0);
+
+ const char* kGarbageStrings[] = {
+ "",
+ R"(naked text)",
+ R"("")",
+ R"(123)",
+ R"("just a string")",
+ R"([])",
+ R"({})",
+ R"({"type":"GET_STATUS"})", // Note: Missing requestId.
+ };
+ for (const char* some_string : kGarbageStrings) {
+ const auto result = sender_outbound()->Send(
+ MakeCastMessage(kPlatformSenderId, kPlatformReceiverId,
+ kReceiverNamespace, some_string));
+ ASSERT_TRUE(result.ok()) << result;
+ }
+}
+
+TEST_F(ApplicationAgentTest, HandlesInvalidCommands) {
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":3,
+ "type":"INVALID_REQUEST",
+ "reason":"INVALID_COMMAND"
+ })";
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ auto result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":3,
+ "type":"FINISH_Q3_OKRS_BY_END_OF_Q3"
+ })"));
+ ASSERT_TRUE(result.ok()) << result;
+}
+
+TEST_F(ApplicationAgentTest, HandlesPings) {
+ constexpr int kNumPings = 3;
+
+ int num_pongs = 0;
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .Times(kNumPings)
+ .WillRepeatedly(Invoke([&num_pongs](CastSocket*, CastMessage message) {
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kHeartbeatNamespace);
+ EXPECT_EQ(json::Parse(R"({"type":"PONG"})").value(), payload);
+ ++num_pongs;
+ }));
+
+ const CastMessage message =
+ MakeCastMessage(kPlatformSenderId, kPlatformReceiverId,
+ kHeartbeatNamespace, R"({"type":"PING"})");
+ for (int i = 0; i < kNumPings; ++i) {
+ const auto result = sender_outbound()->Send(message);
+ ASSERT_TRUE(result.ok()) << result;
+ }
+ EXPECT_EQ(kNumPings, num_pongs);
+}
+
+TEST_F(ApplicationAgentTest, HandlesGetAppAvailability) {
+ // Send the request before any apps have been registered. Expect an
+ // "unavailable" response.
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":548,
+ "responseType":"GET_APP_AVAILABILITY",
+ "availability":{"1A2B3C4D":"APP_UNAVAILABLE"}
+ })";
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ auto result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":548,
+ "type":"GET_APP_AVAILABILITY",
+ "appId":["1A2B3C4D"]
+ })"));
+ ASSERT_TRUE(result.ok()) << result;
+
+ // Register an application.
+ FakeApplication some_app("1A2B3C4D", "Something Doer");
+ agent()->RegisterApplication(&some_app);
+
+ // Send another request, and expect the application to be available.
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":549,
+ "responseType":"GET_APP_AVAILABILITY",
+ "availability":{"1A2B3C4D":"APP_AVAILABLE"}
+ })";
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":549,
+ "type":"GET_APP_AVAILABILITY",
+ "appId":["1A2B3C4D"]
+ })"));
+ ASSERT_TRUE(result.ok()) << result;
+
+ agent()->UnregisterApplication(&some_app);
+}
+
+TEST_F(ApplicationAgentTest, HandlesGetStatus) {
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":123,
+ "type":"RECEIVER_STATUS",
+ "status":{
+ "applications":[
+ {
+ // NOTE: These IDs and the displayName come from |idle_app_|.
+ "sessionId":"E8C28D3C-9ABC-DEF0-1234-000000000001",
+ "appId":"E8C28D3C",
+ "universalAppId":"E8C28D3C",
+ "displayName":"Backdrop",
+ "isIdleScreen":true,
+ "launchedFromCloud":false,
+ "namespaces":[]
+ }
+ ],
+ "userEq":{},
+ "volume":{
+ "controlType":"attenuation",
+ "level":1.0,
+ "muted":false,
+ "stepInterval":0.05
+ }
+ }
+ })";
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ auto result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":123,
+ "type":"GET_STATUS"
+ })"));
+ ASSERT_TRUE(result.ok()) << result;
+}
+
+TEST_F(ApplicationAgentTest, FailsLaunchRequestWithBadAppID) {
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ constexpr char kExpectedJson[] = R"({
+ "requestId":1,
+ "type":"LAUNCH_ERROR",
+ "reason":"NOT_FOUND"
+ })";
+ const Json::Value payload =
+ ValidateAndParseMessage(message, kPlatformReceiverId,
+ kPlatformSenderId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ auto launch_result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":1,
+ "type":"LAUNCH",
+ "appId":"DEADBEEF"
+ })"));
+ ASSERT_TRUE(launch_result.ok()) << launch_result;
+}
+
+TEST_F(ApplicationAgentTest, LaunchesApp_PassesMessages_ThenStopsApp) {
+ StrictMock<FakeApplication> some_app("1A2B3C4D", "Something Doer");
+ constexpr char kAppNamespace[] = "urn:x-cast:com.google.cast.something";
+ some_app.SetSupportedNamespaces({std::string(kAppNamespace)});
+ agent()->RegisterApplication(&some_app);
+
+ // Phase 1: Sender sends a LAUNCH request, which causes the idle app to stop
+ // and the receiver app to launch. The receiver (ApplicationAgent) broadcasts
+ // a RECEIVER_STATUS to indicate the app is running; and both the receiver app
+ // and the sender will get a copy of it.
+ Sequence phase1;
+ MessagePort* port_for_app = nullptr;
+ EXPECT_CALL(*idle_app(), DidStop()).InSequence(phase1);
+ EXPECT_CALL(some_app, DidLaunch(_, NotNull()))
+ .InSequence(phase1)
+ .WillOnce(Invoke([&](Json::Value params, MessagePort* port) {
+ EXPECT_EQ(json::Parse(R"({"a":1,"b":2})").value(), params);
+ port_for_app = port;
+ port->SetClient(&some_app, some_app.GetSessionId());
+ }));
+ const std::string kRunningAppReceiverStatus = R"({
+ "requestId":0, // Note: 0 for broadcast (no requestor).
+ "type":"RECEIVER_STATUS",
+ "status":{
+ "applications":[
+ {
+ // NOTE: These IDs and the displayName come from |some_app|.
+ "transportId":"1A2B3C4D-9ABC-DEF0-1234-000000000001",
+ "sessionId":"1A2B3C4D-9ABC-DEF0-1234-000000000001",
+ "appId":"1A2B3C4D",
+ "universalAppId":"1A2B3C4D",
+ "displayName":"Something Doer",
+ "isIdleScreen":false,
+ "launchedFromCloud":false,
+ "namespaces":[{"name":"urn:x-cast:com.google.cast.something"}]
+ }
+ ],
+ "userEq":{},
+ "volume":{
+ "controlType":"attenuation",
+ "level":1.0,
+ "muted":false,
+ "stepInterval":0.05
+ }
+ }
+ })";
+ EXPECT_CALL(some_app, OnMessage(_, _, _))
+ .InSequence(phase1)
+ .WillOnce(Invoke([&](const std::string& source_id,
+ const std::string& the_namespace,
+ const std::string& message) {
+ EXPECT_EQ(kPlatformReceiverId, source_id);
+ EXPECT_EQ(kReceiverNamespace, the_namespace);
+ const auto parsed = json::Parse(message);
+ EXPECT_TRUE(parsed.is_value()) << parsed.error();
+ if (parsed.is_value()) {
+ EXPECT_EQ(json::Parse(kRunningAppReceiverStatus).value(),
+ parsed.value());
+ }
+ }));
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .InSequence(phase1)
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ const Json::Value payload = ValidateAndParseMessage(
+ message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kRunningAppReceiverStatus).value(), payload);
+ }));
+ auto launch_result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":17,
+ "type":"LAUNCH",
+ "appId":"1A2B3C4D",
+ "appParams":{"a":1,"b":2},
+ "language":"en-US",
+ "supportedAppTypes":["WEB"]
+ })"));
+ ASSERT_TRUE(launch_result.ok()) << launch_result;
+ Mock::VerifyAndClearExpectations(idle_app());
+ Mock::VerifyAndClearExpectations(&some_app);
+ Mock::VerifyAndClearExpectations(sender_inbound());
+
+ // Phase 2: Sender sends a message to the app, and the receiver app sends a
+ // reply.
+ constexpr char kMessage[] = R"({"type":"FOO","data":"Hello world!"})";
+ constexpr char kReplyMessage[] =
+ R"({"type":"FOO_REPLY","data":"Hi yourself!"})";
+ constexpr char kSenderTransportId[] = "sender-1";
+ Sequence phase2;
+ EXPECT_CALL(some_app, OnMessage(_, _, _))
+ .InSequence(phase2)
+ .WillOnce(Invoke([&](const std::string& source_id,
+ const std::string& the_namespace,
+ const std::string& message) {
+ EXPECT_EQ(kSenderTransportId, source_id);
+ EXPECT_EQ(kAppNamespace, the_namespace);
+ const auto parsed = json::Parse(message);
+ EXPECT_TRUE(parsed.is_value()) << parsed.error();
+ if (parsed.is_value()) {
+ EXPECT_EQ(json::Parse(kMessage).value(), parsed.value());
+ if (port_for_app) {
+ port_for_app->PostMessage(kSenderTransportId, kAppNamespace,
+ kReplyMessage);
+ }
+ }
+ }));
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .InSequence(phase2)
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ const Json::Value payload =
+ ValidateAndParseMessage(message, some_app.GetSessionId(),
+ kSenderTransportId, kAppNamespace);
+ EXPECT_EQ(json::Parse(kReplyMessage).value(), payload);
+ }));
+ auto message_send_result = sender_outbound()->Send(MakeCastMessage(
+ kSenderTransportId, some_app.GetSessionId(), kAppNamespace, kMessage));
+ ASSERT_TRUE(message_send_result.ok()) << message_send_result;
+ Mock::VerifyAndClearExpectations(&some_app);
+ Mock::VerifyAndClearExpectations(sender_inbound());
+
+ // Phase 3: Sender sends a STOP request, which causes the receiver
+ // (ApplicationAgent) to stop the app. Then, the idle app will automatically
+ // be re-launched, and a RECEIVER_STATUS broadcast message will notify the
+ // sender of that.
+ Sequence phase3;
+ EXPECT_CALL(some_app, DidStop()).InSequence(phase3);
+ EXPECT_CALL(*idle_app(), DidLaunch(_, NotNull())).InSequence(phase3);
+ EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
+ .InSequence(phase3)
+ .WillOnce(Invoke([&](CastSocket*, CastMessage message) {
+ const std::string kExpectedJson = R"({
+ "requestId":0, // Note: 0 for broadcast (no requestor).
+ "type":"RECEIVER_STATUS",
+ "status":{
+ "applications":[
+ {
+ // NOTE: These IDs and the displayName come from |idle_app_|.
+ "sessionId":"E8C28D3C-9ABC-DEF0-1234-000000000002",
+ "appId":"E8C28D3C",
+ "universalAppId":"E8C28D3C",
+ "displayName":"Backdrop",
+ "isIdleScreen":true,
+ "launchedFromCloud":false,
+ "namespaces":[]
+ }
+ ],
+ "userEq":{},
+ "volume":{
+ "controlType":"attenuation",
+ "level":1.0,
+ "muted":false,
+ "stepInterval":0.05
+ }
+ }
+ })";
+ const Json::Value payload = ValidateAndParseMessage(
+ message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
+ EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
+ }));
+ auto stop_result = sender_outbound()->Send(MakeCastMessage(
+ kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
+ "requestId":18,
+ "type":"STOP",
+ "sessionId":"1A2B3C4D-9ABC-DEF0-1234-000000000001"
+ })"));
+ ASSERT_TRUE(stop_result.ok()) << stop_result;
+ Mock::VerifyAndClearExpectations(idle_app());
+ Mock::VerifyAndClearExpectations(&some_app);
+ Mock::VerifyAndClearExpectations(sender_inbound());
+
+ agent()->UnregisterApplication(&some_app);
+}
+
+} // namespace
+} // namespace cast
+} // namespace openscreen
diff --git a/cast/sender/cast_platform_client.cc b/cast/sender/cast_platform_client.cc
index 224a58a4..f57adc8b 100644
--- a/cast/sender/cast_platform_client.cc
+++ b/cast/sender/cast_platform_client.cc
@@ -22,24 +22,11 @@ namespace cast {
static constexpr std::chrono::seconds kRequestTimeout = std::chrono::seconds(5);
-namespace {
-
-// TODO(miu): This is duplicated in another teammate's WIP CL. De-dupe this by
-// placing the utility in cast/common.
-std::string MakeRandomSenderId() {
- static auto& rd = *new std::random_device();
- static auto& gen = *new std::mt19937(rd());
- static auto& dist = *new std::uniform_int_distribution<>(1, 1000000);
- return absl::StrCat("sender-", dist(gen));
-}
-
-} // namespace
-
CastPlatformClient::CastPlatformClient(VirtualConnectionRouter* router,
VirtualConnectionManager* manager,
ClockNowFunctionPtr clock,
TaskRunner* task_runner)
- : sender_id_(MakeRandomSenderId()),
+ : sender_id_(MakeUniqueSessionId("sender")),
virtual_conn_router_(router),
virtual_conn_manager_(manager),
clock_(clock),
diff --git a/cast/streaming/sender_session.cc b/cast/streaming/sender_session.cc
index 897e7560..087179dc 100644
--- a/cast/streaming/sender_session.cc
+++ b/cast/streaming/sender_session.cc
@@ -8,15 +8,13 @@
#include <stdint.h>
#include <algorithm>
-#include <chrono>
#include <iterator>
-#include <limits>
-#include <random>
#include <string>
#include <utility>
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
+#include "cast/common/channel/message_util.h"
#include "cast/common/public/message_port.h"
#include "cast/streaming/capture_recommendations.h"
#include "cast/streaming/environment.h"
@@ -144,14 +142,6 @@ bool AreAllValid(const std::vector<AudioCaptureConfig>& audio_configs,
IsValidVideoCaptureConfig);
}
-int GenerateSessionId() {
- static auto& rd = *new std::random_device();
- static auto& gen = *new std::mt19937(rd());
- static auto& dist =
- *new std::uniform_int_distribution<>(1, std::numeric_limits<int>::max());
-
- return dist(gen);
-}
} // namespace
SenderSession::Client::~Client() = default;
@@ -160,18 +150,16 @@ SenderSession::SenderSession(IPAddress remote_address,
Client* const client,
Environment* environment,
MessagePort* message_port)
- : session_id_(GenerateSessionId()),
- remote_address_(remote_address),
+ : remote_address_(remote_address),
client_(client),
environment_(environment),
message_port_(message_port),
packet_router_(environment_) {
- OSP_DCHECK(session_id_ > 0);
OSP_DCHECK(client_);
OSP_DCHECK(message_port_);
OSP_DCHECK(environment_);
- message_port_->SetClient(this, "sender-" + std::to_string(session_id_));
+ message_port_->SetClient(this, MakeUniqueSessionId("sender"));
}
SenderSession::~SenderSession() {
diff --git a/cast/streaming/sender_session.h b/cast/streaming/sender_session.h
index a38ac422..4e8a718b 100644
--- a/cast/streaming/sender_session.h
+++ b/cast/streaming/sender_session.h
@@ -138,9 +138,6 @@ class SenderSession final : public MessagePort::Client {
// Sends a message over the message port.
void SendMessage(Message* message);
- // The cast session ID for this session.
- const int session_id_;
-
// The sender ID of the Receiver for this session.
std::string receiver_sender_id_;