diff options
author | Yuri Wiitala <miu@chromium.org> | 2020-10-05 15:15:08 -0700 |
---|---|---|
committer | Commit Bot <commit-bot@chromium.org> | 2020-10-05 23:11:31 +0000 |
commit | eee0513a73bc7ef3f913865f1b359ec6d29e4308 (patch) | |
tree | 728800c45c43f7129d7bf29435bf30b2af5ae0da /cast | |
parent | 952113a3bdf55baa055d74c216260edb3dce46a6 (diff) | |
download | openscreen-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.cc | 2 | ||||
-rw-r--r-- | cast/common/channel/cast_socket_message_port.h | 2 | ||||
-rw-r--r-- | cast/common/channel/message_util.cc | 11 | ||||
-rw-r--r-- | cast/common/channel/message_util.h | 38 | ||||
-rw-r--r-- | cast/common/channel/virtual_connection.h | 4 | ||||
-rw-r--r-- | cast/common/public/cast_socket.h | 2 | ||||
-rw-r--r-- | cast/receiver/BUILD.gn | 24 | ||||
-rw-r--r-- | cast/receiver/DEPS | 6 | ||||
-rw-r--r-- | cast/receiver/application_agent.cc | 402 | ||||
-rw-r--r-- | cast/receiver/application_agent.h | 169 | ||||
-rw-r--r-- | cast/receiver/application_agent_unittest.cc | 605 | ||||
-rw-r--r-- | cast/sender/cast_platform_client.cc | 15 | ||||
-rw-r--r-- | cast/streaming/sender_session.cc | 18 | ||||
-rw-r--r-- | cast/streaming/sender_session.h | 3 |
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_; |