blob: 0e674a5e2a3b481adc11ade5e5a01eb6991b74ac [file] [log] [blame]
// Copyright 2019 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 "components/mpris/mpris_service.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/process/process.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "components/dbus/dbus_thread_linux.h"
#include "components/mpris/mpris_service_observer.h"
#include "dbus/bus.h"
#include "dbus/exported_object.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
#include "dbus/property.h"
#include "dbus/values_util.h"
namespace mpris {
namespace {
constexpr int kNumMethodsToExport = 14;
} // namespace
#if defined(GOOGLE_CHROME_BUILD)
const char kMprisAPIServiceNamePrefix[] =
"org.mpris.MediaPlayer2.chrome.instance";
#else
const char kMprisAPIServiceNamePrefix[] =
"org.mpris.MediaPlayer2.chromium.instance";
#endif
const char kMprisAPIObjectPath[] = "/org/mpris/MediaPlayer2";
const char kMprisAPIInterfaceName[] = "org.mpris.MediaPlayer2";
const char kMprisAPIPlayerInterfaceName[] = "org.mpris.MediaPlayer2.Player";
// static
MprisService* MprisService::instance_ = nullptr;
// static
MprisService* MprisService::GetInstance() {
return instance_;
}
MprisService::MprisService()
: service_name_(std::string(kMprisAPIServiceNamePrefix) +
std::to_string(base::Process::Current().Pid())) {
DCHECK_EQ(nullptr, instance_);
instance_ = this;
InitializeProperties();
}
MprisService::~MprisService() {
DCHECK_EQ(instance_, this);
instance_ = nullptr;
if (bus_) {
dbus_thread_linux::GetTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&dbus::Bus::ShutdownAndBlock, bus_));
}
}
void MprisService::StartService() {
InitializeDbusInterface();
}
void MprisService::AddObserver(MprisServiceObserver* observer) {
observers_.AddObserver(observer);
// If the service is already ready, inform the observer.
if (service_ready_)
observer->OnServiceReady();
}
void MprisService::RemoveObserver(MprisServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
void MprisService::SetCanGoNext(bool value) {
SetPropertyInternal(media_player2_player_properties_, "CanGoNext",
base::Value(value));
}
void MprisService::SetCanGoPrevious(bool value) {
SetPropertyInternal(media_player2_player_properties_, "CanGoPrevious",
base::Value(value));
}
void MprisService::SetCanPlay(bool value) {
SetPropertyInternal(media_player2_player_properties_, "CanPlay",
base::Value(value));
}
void MprisService::SetCanPause(bool value) {
SetPropertyInternal(media_player2_player_properties_, "CanPause",
base::Value(value));
}
std::string MprisService::GetServiceName() const {
return service_name_;
}
void MprisService::InitializeProperties() {
// org.mpris.MediaPlayer2 interface properties.
media_player2_properties_["CanQuit"] = base::Value(false);
media_player2_properties_["CanRaise"] = base::Value(false);
media_player2_properties_["HasTrackList"] = base::Value(false);
#if defined(GOOGLE_CHROME_BUILD)
media_player2_properties_["Identity"] = base::Value("Chrome");
#else
media_player2_properties_["Identity"] = base::Value("Chromium");
#endif
media_player2_properties_["SupportedUriSchemes"] =
base::Value(base::Value::Type::LIST);
media_player2_properties_["SupportedMimeTypes"] =
base::Value(base::Value::Type::LIST);
// org.mpris.MediaPlayer2.Player interface properties.
media_player2_player_properties_["PlaybackStatus"] = base::Value("Stopped");
media_player2_player_properties_["Rate"] = base::Value(1.0);
media_player2_player_properties_["Metadata"] =
base::Value(base::Value::DictStorage());
media_player2_player_properties_["Volume"] = base::Value(1.0);
media_player2_player_properties_["Position"] = base::Value(0);
media_player2_player_properties_["MinimumRate"] = base::Value(1.0);
media_player2_player_properties_["MaximumRate"] = base::Value(1.0);
media_player2_player_properties_["CanGoNext"] = base::Value(false);
media_player2_player_properties_["CanGoPrevious"] = base::Value(false);
media_player2_player_properties_["CanPlay"] = base::Value(false);
media_player2_player_properties_["CanPause"] = base::Value(false);
media_player2_player_properties_["CanSeek"] = base::Value(false);
media_player2_player_properties_["CanControl"] = base::Value(true);
}
void MprisService::InitializeDbusInterface() {
// Bus may be set for testing.
if (!bus_) {
dbus::Bus::Options bus_options;
bus_options.bus_type = dbus::Bus::SESSION;
bus_options.connection_type = dbus::Bus::PRIVATE;
bus_options.dbus_task_runner = dbus_thread_linux::GetTaskRunner();
bus_ = base::MakeRefCounted<dbus::Bus>(bus_options);
}
exported_object_ =
bus_->GetExportedObject(dbus::ObjectPath(kMprisAPIObjectPath));
int num_methods_attempted_to_export = 0;
// Helper lambdas for exporting methods while keeping track of the number of
// exported methods.
auto export_method =
[&](const std::string& interface_name, const std::string& method_name,
dbus::ExportedObject::MethodCallCallback method_call_callback) {
exported_object_->ExportMethod(
interface_name, method_name, method_call_callback,
base::BindRepeating(&MprisService::OnExported,
base::Unretained(this)));
num_methods_attempted_to_export++;
};
auto export_unhandled_method = [&](const std::string& interface_name,
const std::string& method_name) {
export_method(
interface_name, method_name,
base::BindRepeating(&MprisService::DoNothing, base::Unretained(this)));
};
// Set up org.mpris.MediaPlayer2 interface.
// https://specifications.freedesktop.org/mpris-spec/2.2/Media_Player.html
export_unhandled_method(kMprisAPIInterfaceName, "Raise");
export_unhandled_method(kMprisAPIInterfaceName, "Quit");
// Set up org.mpris.MediaPlayer2.Player interface.
// https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html
export_method(
kMprisAPIPlayerInterfaceName, "Next",
base::BindRepeating(&MprisService::Next, base::Unretained(this)));
export_method(
kMprisAPIPlayerInterfaceName, "Previous",
base::BindRepeating(&MprisService::Previous, base::Unretained(this)));
export_method(
kMprisAPIPlayerInterfaceName, "Pause",
base::BindRepeating(&MprisService::Pause, base::Unretained(this)));
export_method(
kMprisAPIPlayerInterfaceName, "PlayPause",
base::BindRepeating(&MprisService::PlayPause, base::Unretained(this)));
export_method(
kMprisAPIPlayerInterfaceName, "Stop",
base::BindRepeating(&MprisService::Stop, base::Unretained(this)));
export_method(
kMprisAPIPlayerInterfaceName, "Play",
base::BindRepeating(&MprisService::Play, base::Unretained(this)));
export_unhandled_method(kMprisAPIPlayerInterfaceName, "Seek");
export_unhandled_method(kMprisAPIPlayerInterfaceName, "SetPosition");
export_unhandled_method(kMprisAPIPlayerInterfaceName, "OpenUri");
// Set up org.freedesktop.DBus.Properties interface.
// https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties
export_method(dbus::kPropertiesInterface, dbus::kPropertiesGetAll,
base::BindRepeating(&MprisService::GetAllProperties,
base::Unretained(this)));
export_method(
dbus::kPropertiesInterface, dbus::kPropertiesGet,
base::BindRepeating(&MprisService::GetProperty, base::Unretained(this)));
export_unhandled_method(dbus::kPropertiesInterface, dbus::kPropertiesSet);
DCHECK_EQ(kNumMethodsToExport, num_methods_attempted_to_export);
}
void MprisService::OnExported(const std::string& interface_name,
const std::string& method_name,
bool success) {
if (success)
num_methods_exported_++;
// Still waiting for more methods to finish exporting.
if (num_methods_exported_ < kNumMethodsToExport)
return;
bus_->RequestOwnership(
service_name_, dbus::Bus::ServiceOwnershipOptions::REQUIRE_PRIMARY,
base::BindRepeating(&MprisService::OnOwnership, base::Unretained(this)));
}
void MprisService::OnOwnership(const std::string& service_name, bool success) {
DCHECK(success);
service_ready_ = true;
for (MprisServiceObserver& obs : observers_)
obs.OnServiceReady();
}
void MprisService::Next(dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnNext();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::Previous(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnPrevious();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::Pause(dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnPause();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::PlayPause(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnPlayPause();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::Stop(dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnStop();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::Play(dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
for (MprisServiceObserver& obs : observers_)
obs.OnPlay();
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
void MprisService::DoNothing(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
response_sender.Run(dbus::Response::FromMethodCall(method_call));
}
// org.freedesktop.DBus.Properties.GetAll(in STRING interface_name,
// out DICT<STRING,VARIANT> props);
void MprisService::GetAllProperties(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
dbus::MessageReader reader(method_call);
std::string interface;
if (!reader.PopString(&interface)) {
response_sender.Run(nullptr);
return;
}
std::unique_ptr<dbus::Response> response =
dbus::Response::FromMethodCall(method_call);
dbus::MessageWriter writer(response.get());
if (interface == kMprisAPIInterfaceName) {
AddPropertiesToWriter(&writer, media_player2_properties_);
} else if (interface == kMprisAPIPlayerInterfaceName) {
AddPropertiesToWriter(&writer, media_player2_player_properties_);
} else if (interface == dbus::kPropertiesInterface) {
// There are no properties to give for this interface.
PropertyMap empty_properties_map;
AddPropertiesToWriter(&writer, empty_properties_map);
} else {
// The given interface is not supported, so return a null response.
response_sender.Run(nullptr);
return;
}
response_sender.Run(std::move(response));
}
// org.freedesktop.DBus.Properties.Get(in STRING interface_name,
// in STRING property_name,
// out VARIANT value);
void MprisService::GetProperty(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
dbus::MessageReader reader(method_call);
std::string interface;
if (!reader.PopString(&interface)) {
response_sender.Run(nullptr);
return;
}
std::string property_name;
if (!reader.PopString(&property_name)) {
response_sender.Run(nullptr);
return;
}
std::unique_ptr<dbus::Response> response =
dbus::Response::FromMethodCall(method_call);
dbus::MessageWriter writer(response.get());
bool success = false;
if (interface == kMprisAPIInterfaceName) {
auto property_iter = media_player2_properties_.find(property_name);
if (property_iter != media_player2_properties_.end()) {
dbus::AppendValueDataAsVariant(&writer, property_iter->second);
success = true;
}
} else if (interface == kMprisAPIPlayerInterfaceName) {
auto property_iter = media_player2_player_properties_.find(property_name);
if (property_iter != media_player2_player_properties_.end()) {
dbus::AppendValueDataAsVariant(&writer, property_iter->second);
success = true;
}
}
// If we don't support the given property, return a null response.
if (!success) {
response_sender.Run(nullptr);
return;
}
response_sender.Run(std::move(response));
}
void MprisService::AddPropertiesToWriter(dbus::MessageWriter* writer,
const PropertyMap& properties) {
DCHECK(writer);
dbus::MessageWriter array_writer(nullptr);
dbus::MessageWriter dict_entry_writer(nullptr);
writer->OpenArray("{sv}", &array_writer);
for (auto& property : properties) {
array_writer.OpenDictEntry(&dict_entry_writer);
dict_entry_writer.AppendString(property.first);
dbus::AppendValueDataAsVariant(&dict_entry_writer, property.second);
array_writer.CloseContainer(&dict_entry_writer);
}
writer->CloseContainer(&array_writer);
}
void MprisService::SetPropertyInternal(PropertyMap& property_map,
const std::string& property_name,
const base::Value& new_value) {
if (property_map[property_name] == new_value)
return;
property_map[property_name] = new_value.Clone();
PropertyMap changed_properties;
changed_properties[property_name] = new_value.Clone();
EmitPropertiesChangedSignal(changed_properties);
}
void MprisService::EmitPropertiesChangedSignal(
const PropertyMap& changed_properties) {
if (!bus_ || !exported_object_)
return;
// |signal| follows the PropertiesChanged API:
// org.freedesktop.DBus.Properties.PropertiesChanged(
// STRING interface_name,
// DICT<STRING,VARIANT> changed_properties,
// ARRAY<STRING> invalidated_properties);
dbus::Signal signal(dbus::kPropertiesInterface, dbus::kPropertiesChanged);
dbus::MessageWriter writer(&signal);
writer.AppendString(kMprisAPIPlayerInterfaceName);
AddPropertiesToWriter(&writer, changed_properties);
std::vector<std::string> empty_invalidated_properties;
writer.AppendArrayOfStrings(empty_invalidated_properties);
exported_object_->SendSignal(&signal);
}
} // namespace mpris