blob: 5bb63b2bee29719a2bd65cd808fb695dfd3686e1 [file] [log] [blame] [edit]
/**
* Copyright 2020 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "Module.h"
#include <interfaces/IMemory.h>
#include <interfaces/IBrowser.h>
#include <interfaces/IDictionary.h>
#if defined(PLUGIN_COBALT_ENABLE_FOCUS_IFACE) && PLUGIN_COBALT_ENABLE_FOCUS_IFACE
#include <interfaces/IFocus.h>
#endif
extern "C" {
int StarboardMain(int argc, char **argv);
void SbRdkHandleDeepLink(const char* link);
void SbRdkSuspend();
void SbRdkResume();
void SbRdkPause();
void SbRdkUnpause();
void SbRdkQuit();
void SbRdkSetSetting(const char* key, const char* json);
int SbRdkGetSetting(const char* key, char** out_json);
typedef int (*SbRdkCallbackFunc)(void *user_data);
void SbRdkSetConcealRequestHandler(SbRdkCallbackFunc cb, void* user_data);
void SbRdkSetCobaltExitStrategy(const char* strategy);
} // extern "C"
namespace WPEFramework {
namespace Plugin {
static constexpr char kDefaultContentDir[] =
"/usr/share/content/data:"
"/media/apps/libcobalt/usr/share/content/data:"
"/tmp/libcobalt/usr/share/content/data";
static void SetThunderAccessPointIfNeeded() {
const std::string envName = _T("THUNDER_ACCESS");
std::string envVal;
if (Core::SystemInfo::GetEnvironment(envName, envVal))
return;
Core::File file("/etc/WPEFramework/config.json", false);
if (!file.Open(true))
return;
Core::JSON::DecUInt16 port;
Core::JSON::String binding;
Core::JSON::Container config;
config.Add("port", &port);
config.Add("binding", &binding);
if (config.IElement::FromFile(file)) {
envVal = binding.Value() + ":" + std::to_string(port.Value());
Core::SystemInfo::SetEnvironment("THUNDER_ACCESS", envVal);
}
file.Close();
}
enum StateChangeCommand : uint16_t {
SUSPEND = PluginHost::IStateControl::SUSPEND,
RESUME = PluginHost::IStateControl::RESUME,
BACKGROUND
};
class CobaltImplementation:
public Exchange::IBrowser,
public PluginHost::IStateControl,
public Exchange::IDictionary
#if defined(PLUGIN_COBALT_ENABLE_FOCUS_IFACE) && PLUGIN_COBALT_ENABLE_FOCUS_IFACE
, public Exchange::IFocus
#endif
{
private:
class Config: public Core::JSON::Container {
private:
Config(const Config&);
Config& operator=(const Config&);
public:
Config()
: Core::JSON::Container()
, Url()
, ClientIdentifier()
, Language()
, ContentDir()
, PreloadEnabled()
, AutoSuspendDelay() {
Add(_T("url"), &Url);
Add(_T("clientidentifier"), &ClientIdentifier);
Add(_T("language"), &Language);
Add(_T("contentdir"), &ContentDir);
Add(_T("gstdebug"), &GstDebug);
Add(_T("preload"), &PreloadEnabled);
Add(_T("autosuspenddelay"), &AutoSuspendDelay);
Add(_T("systemproperties"), &SystemProperties);
Add(_T("closurepolicy"), &ClosurePolicy);
}
~Config() {
}
public:
Core::JSON::String Url;
Core::JSON::String ClientIdentifier;
Core::JSON::String Language;
Core::JSON::String ContentDir;
Core::JSON::String GstDebug;
Core::JSON::Boolean PreloadEnabled;
Core::JSON::DecUInt16 AutoSuspendDelay;
Core::JSON::VariantContainer SystemProperties;
Core::JSON::String ClosurePolicy;
};
class NotificationSink: public Core::Thread {
private:
NotificationSink() = delete;
NotificationSink(const NotificationSink&) = delete;
NotificationSink& operator=(const NotificationSink&) = delete;
public:
NotificationSink(CobaltImplementation &parent) :
_parent(parent), _command(
StateChangeCommand::SUSPEND) {
}
virtual ~NotificationSink() {
Stop();
Wait(Thread::STOPPED | Thread::BLOCKED, Core::infinite);
}
public:
void RequestForStateChange(
const StateChangeCommand command) {
_lock.Lock();
_command = command;
_lock.Unlock();
Run();
}
private:
virtual uint32_t Worker() {
bool success = false;
_lock.Lock();
const StateChangeCommand command = _command;
_lock.Unlock();
if ((IsRunning() == true) && (success == false)) {
success = _parent.RequestForStateChange(command);
}
Block();
_parent.StateChangeCompleted(success, command);
if (success == true) {
_lock.Lock();
bool completed = (command == _command);
_lock.Unlock();
// Spin one more time
if (!completed)
Run();
}
return (Core::infinite);
}
private:
CobaltImplementation &_parent;
StateChangeCommand _command;
mutable Core::CriticalSection _lock;
};
class DelayedSuspend
{
private:
DelayedSuspend() = delete;
DelayedSuspend(const DelayedSuspend&) = delete;
DelayedSuspend& operator=(const DelayedSuspend&) = delete;
private:
bool _scheduled;
CobaltImplementation &_parent;
friend Core::ThreadPool::JobType<DelayedSuspend&>;
Core::WorkerPool::JobType<DelayedSuspend&> _worker;
void Dispatch()
{
_parent.RequestDelayedSuspend();
_scheduled = false;
}
public:
DelayedSuspend(CobaltImplementation &parent)
: _scheduled(false)
, _parent(parent)
, _worker(*this)
{
}
bool IsScheduled() const
{
return _scheduled;
}
void Schedule(const Core::Time& time)
{
_scheduled = _worker.Schedule(time);
}
void Cancel()
{
_worker.Revoke();
_scheduled = false;
}
};
class CobaltWindow : public Core::Thread {
private:
CobaltWindow(const CobaltWindow&) = delete;
CobaltWindow& operator=(const CobaltWindow&) = delete;
public:
CobaltWindow(CobaltImplementation& parent)
: Core::Thread(0, _T("Cobalt"))
, _url("https://www.youtube.com/tv")
, _parent(parent)
{
}
virtual ~CobaltWindow()
{
Block();
SbRdkQuit();
Wait(Thread::BLOCKED | Thread::STOPPED | Thread::STOPPING, Core::infinite);
exit(_exitCode);
}
uint32_t Configure(PluginHost::IShell* service) {
if (IsRunning() == true)
return Core::ERROR_ILLEGAL_STATE;
uint32_t result = Core::ERROR_NONE;
string config_line = service->ConfigLine();
SYSLOG(Logging::Notification, (_T("Cobalt config line: --- %s ---\n"), config_line.c_str()));
Config config;
config.FromString(config_line);
Core::Directory(service->PersistentPath().c_str()).CreatePath();
Core::SystemInfo::SetEnvironment(_T("HOME"), service->PersistentPath());
Core::SystemInfo::SetEnvironment(_T("COBALT_TEMP"), service->VolatilePath());
if (config.ClientIdentifier.IsSet() == true) {
string value(service->Callsign() + ',' + config.ClientIdentifier.Value());
Core::SystemInfo::SetEnvironment(_T("CLIENT_IDENTIFIER"), value);
Core::SystemInfo::SetEnvironment(_T("WAYLAND_DISPLAY"), config.ClientIdentifier.Value());
} else {
Core::SystemInfo::SetEnvironment(_T("CLIENT_IDENTIFIER"), service->Callsign());
}
SetThunderAccessPointIfNeeded();
if (config.Url.IsSet() == true) {
_url = config.Url.Value();
}
if (config.Language.IsSet() == true) {
string lang = config.Language.Value();
Core::SystemInfo::SetEnvironment(_T("LANG"), lang);
}
if (config.ContentDir.IsSet() == true) {
string contentdir = config.ContentDir.Value();
Core::SystemInfo::SetEnvironment(_T("COBALT_CONTENT_DIR"), contentdir);
} else {
Core::SystemInfo::SetEnvironment(_T("COBALT_CONTENT_DIR"), kDefaultContentDir);
}
{
string envVal, gstDebug = "gstplayer:4,2";
if (Core::SystemInfo::GetEnvironment(_T("GST_DEBUG"), envVal) && !envVal.empty())
gstDebug = "," + envVal;
if (config.GstDebug.IsSet() == true)
gstDebug += "," + config.GstDebug.Value();
Core::SystemInfo::SetEnvironment(_T("GST_DEBUG"), gstDebug);
}
if (config.PreloadEnabled.IsSet() == true) {
_preloadEnabled = config.PreloadEnabled.Value();
}
if (config.AutoSuspendDelay.IsSet() == true) {
_autoSuspendDelayInSeconds = config.AutoSuspendDelay.Value();
}
if (config.SystemProperties.IsSet() == true) {
std::string properties;
if (config.SystemProperties.ToString(properties))
SbRdkSetSetting("systemproperties", properties.c_str());
}
SYSLOG(Logging::Notification, (_T("Preload is set to: %s\n"), _preloadEnabled ? "true" : "false"));
if (config.ClosurePolicy.IsSet() == true) {
#if defined(PLUGIN_COBALT_ENABLE_CLOSUREPOLICY) && PLUGIN_COBALT_ENABLE_CLOSUREPOLICY
SbRdkSetCobaltExitStrategy(config.ClosurePolicy.Value().data());
#else
SYSLOG(Logging::Notification, (_T("Ignore 'closurepolicy' configuration, support is disabled\n")));
#endif
}
SbRdkSetConcealRequestHandler([](void* data)-> int {
CobaltWindow* window = reinterpret_cast<CobaltWindow*>(data);
window->_parent.OnConcealRequest();
return 0;
}, this);
Run();
return result;
}
bool Suspend(const bool suspend)
{
if (suspend == true) {
SbRdkSuspend();
}
else {
SbRdkResume();
}
return (true);
}
bool Pause(const bool paused)
{
if (paused == true) {
SbRdkPause();
}
else {
SbRdkUnpause();
}
return (true);
}
string Url() const { return _url; }
bool IsPreloadEnabled() const { return _preloadEnabled; }
uint16_t AutoSuspendDelayInSeconds() const { return _autoSuspendDelayInSeconds; }
private:
bool Initialize() override
{
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGCONT);
pthread_sigmask(SIG_UNBLOCK, &mask, nullptr);
return (true);
}
uint32_t Worker() override
{
const std::string cmdURL = "--url=" + _url;
std::vector<const char*> argv;
argv.push_back("Cobalt");
if (!_url.empty())
argv.push_back(cmdURL.c_str());
if (IsPreloadEnabled())
argv.push_back("--preload");
{
std::string args_str;
for (const char* arg: argv) {
if (!args_str.empty())
args_str += " ";
args_str += arg;
}
SYSLOG(Logging::Notification, (_T("StarboardMain args: %s\n"), args_str.c_str()));
}
if (IsRunning() == true)
_exitCode = StarboardMain(argv.size(), const_cast<char**>(argv.data()));
if (IsRunning() == true) // app initiated exit
_parent.StateChange(PluginHost::IStateControl::EXITED);
Block();
return (Core::infinite);
}
int _exitCode { 0 };
string _url;
CobaltImplementation &_parent;
bool _preloadEnabled { false };
uint16_t _autoSuspendDelayInSeconds { 30 };
};
private:
CobaltImplementation(const CobaltImplementation&) = delete;
CobaltImplementation& operator=(const CobaltImplementation&) = delete;
public:
CobaltImplementation() :
_window(*this),
_adminLock(),
_state(PluginHost::IStateControl::UNINITIALIZED),
_statePending(PluginHost::IStateControl::UNINITIALIZED),
_cobaltClients(),
_stateControlClients(),
_sink(*this),
_delayedSuspend(*this) {
}
virtual ~CobaltImplementation() {
}
virtual uint32_t Configure(PluginHost::IShell *service) {
uint32_t result = _window.Configure(service);
if (_window.IsPreloadEnabled()) {
_state = PluginHost::IStateControl::SUSPENDED;
_statePending = PluginHost::IStateControl::SUSPENDED;
_delayedSuspend.Schedule(Core::Time::Now().Add(_window.AutoSuspendDelayInSeconds() * 1000));
} else {
_state = PluginHost::IStateControl::RESUMED;
}
return (result);
}
virtual void SetURL(const string &URL) override {
SYSLOG(Logging::Notification, (_T("deeplink=%s\n"), URL.c_str()));
SbRdkHandleDeepLink(URL.c_str());
}
virtual string GetURL() const override {
return _window.Url();
}
virtual uint32_t GetFPS() const override {
return 0;
}
virtual void Hide(const bool hidden) {
}
virtual void Register(Exchange::IBrowser::INotification *sink) {
_adminLock.Lock();
// Make sure a sink is not registered multiple times.
ASSERT(
std::find(_cobaltClients.begin(), _cobaltClients.end(), sink)
== _cobaltClients.end());
_cobaltClients.push_back(sink);
sink->AddRef();
_adminLock.Unlock();
}
virtual void Unregister(Exchange::IBrowser::INotification *sink) {
_adminLock.Lock();
std::list<Exchange::IBrowser::INotification*>::iterator index(
std::find(_cobaltClients.begin(), _cobaltClients.end(), sink));
// Make sure you do not unregister something you did not register !!!
ASSERT(index != _cobaltClients.end());
if (index != _cobaltClients.end()) {
(*index)->Release();
_cobaltClients.erase(index);
}
_adminLock.Unlock();
}
virtual void Register(PluginHost::IStateControl::INotification *sink) {
_adminLock.Lock();
// Make sure a sink is not registered multiple times.
ASSERT(
std::find(_stateControlClients.begin(),
_stateControlClients.end(), sink)
== _stateControlClients.end());
_stateControlClients.push_back(sink);
sink->AddRef();
_adminLock.Unlock();
}
virtual void Unregister(PluginHost::IStateControl::INotification *sink) {
_adminLock.Lock();
std::list<PluginHost::IStateControl::INotification*>::iterator index(
std::find(_stateControlClients.begin(),
_stateControlClients.end(), sink));
// Make sure you do not unregister something you did not register !!!
ASSERT(index != _stateControlClients.end());
if (index != _stateControlClients.end()) {
(*index)->Release();
_stateControlClients.erase(index);
}
_adminLock.Unlock();
}
virtual PluginHost::IStateControl::state State() const {
return (_state);
}
virtual uint32_t Request(const PluginHost::IStateControl::command command) {
uint32_t result = Core::ERROR_ILLEGAL_STATE;
_adminLock.Lock();
if (_state == PluginHost::IStateControl::UNINITIALIZED || _state == PluginHost::IStateControl::EXITED) {
ASSERT(false);
} else {
switch (command) {
case PluginHost::IStateControl::SUSPEND:
if (_state == PluginHost::IStateControl::RESUMED || _statePending == PluginHost::IStateControl::RESUMED) {
_statePending = PluginHost::IStateControl::SUSPENDED;
_sink.RequestForStateChange(
StateChangeCommand::SUSPEND);
}
result = Core::ERROR_NONE;
break;
case PluginHost::IStateControl::RESUME:
if (_state == PluginHost::IStateControl::SUSPENDED || _statePending == PluginHost::IStateControl::SUSPENDED) {
_statePending = PluginHost::IStateControl::RESUMED;
_sink.RequestForStateChange(
StateChangeCommand::RESUME);
}
if (_delayedSuspend.IsScheduled()) {
_delayedSuspend.Cancel();
}
result = Core::ERROR_NONE;
break;
default:
break;
}
}
_adminLock.Unlock();
return result;
}
void StateChangeCompleted(bool success,
const StateChangeCommand request) {
if (success) {
switch (request) {
case StateChangeCommand::RESUME:
_adminLock.Lock();
if (_state != PluginHost::IStateControl::RESUMED) {
StateChange(PluginHost::IStateControl::RESUMED);
}
_adminLock.Unlock();
break;
case StateChangeCommand::SUSPEND:
_adminLock.Lock();
if (_state != PluginHost::IStateControl::SUSPENDED) {
StateChange(PluginHost::IStateControl::SUSPENDED);
}
_adminLock.Unlock();
break;
case StateChangeCommand::BACKGROUND:
break;
default:
ASSERT(false);
break;
}
} else {
StateChange(PluginHost::IStateControl::EXITED);
}
}
void RequestDelayedSuspend() {
_adminLock.Lock();
if (_state == PluginHost::IStateControl::SUSPENDED && _statePending == PluginHost::IStateControl::SUSPENDED) {
_sink.RequestForStateChange(StateChangeCommand::SUSPEND);
}
_adminLock.Unlock();
}
#if defined(PLUGIN_COBALT_ENABLE_FOCUS_IFACE) && PLUGIN_COBALT_ENABLE_FOCUS_IFACE
// IFocus
uint32_t Focused(const bool focused) override {
_adminLock.Lock();
if (_state == PluginHost::IStateControl::RESUMED || _statePending == PluginHost::IStateControl::RESUMED) {
if (focused)
_sink.RequestForStateChange(StateChangeCommand::RESUME);
else
_sink.RequestForStateChange(StateChangeCommand::BACKGROUND);
}
_adminLock.Unlock();
return Core::ERROR_NONE;
};
#endif
// IDictionary iface
void Register(const string& nameSpace, struct IDictionary::INotification* sink) override { }
void Unregister(const string& nameSpace, struct IDictionary::INotification* sink) override { }
IIterator* Get(const string& nameSpace) const override { return nullptr; }
bool Get(const string& nameSpace, const string& key, string& value /* @out */) const override {
if (nameSpace == "settings") {
if (key == "accessibility") {
char* json = nullptr;
if (SbRdkGetSetting(key.c_str(), &json) == 0) {
value.assign(json);
free(json);
return true;
}
}
}
return false;
}
bool Set(const string& nameSpace, const string& key, const string& value) override {
if (nameSpace == "settings") {
if (key == "accessibility") {
SbRdkSetSetting(key.c_str(), value.c_str());
return true;
}
}
return false;
}
void OnConcealRequest() {
// Device lifecycle tests from YTS expect 'suspend' behavior on 'conceal'
Request(PluginHost::IStateControl::SUSPEND);
NotifyClosure();
}
BEGIN_INTERFACE_MAP (CobaltImplementation)
INTERFACE_ENTRY (Exchange::IBrowser)
INTERFACE_ENTRY (PluginHost::IStateControl)
INTERFACE_ENTRY (Exchange::IDictionary)
#if defined(PLUGIN_COBALT_ENABLE_FOCUS_IFACE) && PLUGIN_COBALT_ENABLE_FOCUS_IFACE
INTERFACE_ENTRY (Exchange::IFocus)
#endif
END_INTERFACE_MAP
private:
static const char* ToString(const StateChangeCommand command) {
switch(command) {
case StateChangeCommand::SUSPEND:
return "suspend";
case StateChangeCommand::RESUME:
return "resume";
case StateChangeCommand::BACKGROUND:
return "background";
default:
break;
}
return "unknown";
}
inline bool RequestForStateChange(
const StateChangeCommand command) {
bool result = false;
SYSLOG(Logging::Notification, (_T("Cobalt request state change -> %s\n"), ToString(command)));
switch (command) {
case StateChangeCommand::SUSPEND: {
if (_window.Suspend(true) == true) {
result = true;
}
break;
}
case StateChangeCommand::RESUME: {
// implies unpause
if (_window.Suspend(false) == true) {
result = true;
}
break;
}
case StateChangeCommand::BACKGROUND: {
if (_window.Pause(true) == true) {
result = true;
}
break;
}
default:
ASSERT(false);
break;
}
return result;
}
void StateChange(const PluginHost::IStateControl::state newState) {
_adminLock.Lock();
_state = newState;
_statePending = PluginHost::IStateControl::UNINITIALIZED;
std::list<PluginHost::IStateControl::INotification*>::iterator index(
_stateControlClients.begin());
while (index != _stateControlClients.end()) {
(*index)->StateChange(newState);
index++;
}
_adminLock.Unlock();
}
void NotifyClosure()
{
_adminLock.Lock();
std::list<Exchange::IBrowser::INotification*>::iterator index(_cobaltClients.begin());
while (index != _cobaltClients.end()) {
(*index)->Closure();
index++;
}
_adminLock.Unlock();
}
private:
CobaltWindow _window;
mutable Core::CriticalSection _adminLock;
PluginHost::IStateControl::state _state;
PluginHost::IStateControl::state _statePending;
std::list<Exchange::IBrowser::INotification*> _cobaltClients;
std::list<PluginHost::IStateControl::INotification*> _stateControlClients;
NotificationSink _sink;
DelayedSuspend _delayedSuspend;
};
SERVICE_REGISTRATION(CobaltImplementation, 1, 0);
} // namespace Plugin
namespace Cobalt {
class MemoryObserverImpl: public Exchange::IMemory {
private:
MemoryObserverImpl();
MemoryObserverImpl(const MemoryObserverImpl&);
MemoryObserverImpl& operator=(const MemoryObserverImpl&);
public:
MemoryObserverImpl(const RPC::IRemoteConnection* connection) :
_main(connection == nullptr ? Core::ProcessInfo().Id() : connection->RemoteId()) {
}
~MemoryObserverImpl() {
}
public:
virtual uint64_t Resident() const {
return _main.Resident();
}
virtual uint64_t Allocated() const {
return _main.Allocated();
}
virtual uint64_t Shared() const {
return _main.Shared();
}
virtual uint8_t Processes() const {
return (IsOperational() ? 1 : 0);
}
virtual const bool IsOperational() const {
return _main.IsActive();
}
BEGIN_INTERFACE_MAP (MemoryObserverImpl)
INTERFACE_ENTRY (Exchange::IMemory)
END_INTERFACE_MAP
private:
Core::ProcessInfo _main;
};
Exchange::IMemory* MemoryObserver(const RPC::IRemoteConnection* connection) {
ASSERT(connection != nullptr);
Exchange::IMemory* result = Core::Service<MemoryObserverImpl>::Create<Exchange::IMemory>(connection);
return (result);
}
} // namespace Cobalt
} // namespace WPEFramework