1 /* Spa Bluez5 AVRCP Player
2  *
3  * Copyright © 2021 Pauli Virtanen
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a
6  * copy of this software and associated documentation files (the "Software"),
7  * to deal in the Software without restriction, including without limitation
8  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9  * and/or sell copies of the Software, and to permit persons to whom the
10  * Software is furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice (including the next
13  * paragraph) shall be included in all copies or substantial portions of the
14  * Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
19  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22  * DEALINGS IN THE SOFTWARE.
23  */
24 
25 #include <errno.h>
26 #include <stdbool.h>
27 #include <dbus/dbus.h>
28 
29 #include <spa/utils/string.h>
30 
31 #include "defs.h"
32 #include "player.h"
33 
34 #define PLAYER_OBJECT_PATH_BASE	"/media_player"
35 
36 #define PLAYER_INTERFACE	"org.mpris.MediaPlayer2.Player"
37 
38 #define PLAYER_INTROSPECT_XML						\
39 	DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE			\
40 	"<node>"							\
41 	" <interface name='" PLAYER_INTERFACE "'>"			\
42 	"  <property name='PlaybackStatus' type='s' access='read'/>"	\
43 	" </interface>"							\
44 	" <interface name='" DBUS_INTERFACE_PROPERTIES "'>"		\
45 	"   <method name='Get'>"					\
46 	"     <arg name='interface' type='s' direction='in' />"		\
47 	"     <arg name='name' type='s' direction='in' />"		\
48 	"     <arg name='value' type='v' direction='out' />"		\
49 	"   </method>"							\
50 	"   <method name='Set'>"					\
51 	"     <arg name='interface' type='s' direction='in' />"		\
52 	"     <arg name='name' type='s' direction='in' />"		\
53 	"     <arg name='value' type='v' direction='in' />"		\
54 	"   </method>"							\
55 	"   <method name='GetAll'>"					\
56 	"     <arg name='interface' type='s' direction='in' />"		\
57 	"     <arg name='properties' type='a{sv}' direction='out' />"	\
58 	"   </method>"							\
59 	"   <signal name='PropertiesChanged'>"				\
60 	"     <arg name='interface' type='s' />"			\
61 	"     <arg name='changed_properties' type='a{sv}' />"		\
62 	"     <arg name='invalidated_properties' type='as' />"		\
63 	"   </signal>"							\
64 	" </interface>"							\
65 	" <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>"		\
66 	"  <method name='Introspect'>"					\
67 	"   <arg name='xml' type='s' direction='out'/>"			\
68 	"  </method>"							\
69 	" </interface>"							\
70 	"</node>"
71 
72 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player");
73 #undef SPA_LOG_TOPIC_DEFAULT
74 #define SPA_LOG_TOPIC_DEFAULT &log_topic
75 
76 #define MAX_PROPERTIES 1
77 
78 struct impl {
79 	struct spa_bt_player this;
80 	DBusConnection *conn;
81 	char *path;
82 	struct spa_log *log;
83 	struct spa_dict_item properties_items[MAX_PROPERTIES];
84 	struct spa_dict properties;
85 	unsigned int playing_count;
86 };
87 
88 static size_t instance_counter = 0;
89 
properties_get(struct impl * impl,DBusMessage * m)90 static DBusMessage *properties_get(struct impl *impl, DBusMessage *m)
91 {
92 	const char *iface, *name;
93 	size_t j;
94 
95         if (!dbus_message_get_args(m, NULL,
96                                         DBUS_TYPE_STRING, &iface,
97                                         DBUS_TYPE_STRING, &name,
98                                         DBUS_TYPE_INVALID))
99                 return NULL;
100 
101 	if (!spa_streq(iface, PLAYER_INTERFACE))
102 		return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
103 				"No such interface");
104 
105 	for (j = 0; j < impl->properties.n_items; ++j) {
106 		const struct spa_dict_item *item = &impl->properties.items[j];
107 		if (spa_streq(item->key, name)) {
108 			DBusMessage *r;
109 			DBusMessageIter i, v;
110 
111 			r = dbus_message_new_method_return(m);
112 			if (r == NULL)
113 				return NULL;
114 
115 			dbus_message_iter_init_append(r, &i);
116 			dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
117 					"s", &v);
118 			dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
119 					&item->value);
120 			dbus_message_iter_close_container(&i, &v);
121 			return r;
122 		}
123 	}
124 
125 	return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
126 			"No such property");
127 }
128 
append_properties(struct impl * impl,DBusMessageIter * i)129 static void append_properties(struct impl *impl, DBusMessageIter *i)
130 {
131 	DBusMessageIter d, e, v;
132 	size_t j;
133 
134         dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY,
135                         DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
136                         DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
137                         DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d);
138 
139 	for (j = 0; j < impl->properties.n_items; ++j) {
140 		const struct spa_dict_item *item = &impl->properties.items[j];
141 
142 		spa_log_debug(impl->log, "player %s: %s=%s", impl->path,
143 				item->key, item->value);
144 
145 		dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e);
146 		dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key);
147 		dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v);
148 		dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value);
149 		dbus_message_iter_close_container(&e, &v);
150 		dbus_message_iter_close_container(&d, &e);
151 	}
152 
153 	dbus_message_iter_close_container(i, &d);
154 }
155 
properties_get_all(struct impl * impl,DBusMessage * m)156 static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m)
157 {
158 	const char *iface, *name;
159 	DBusMessage *r;
160 	DBusMessageIter i;
161 
162         if (!dbus_message_get_args(m, NULL,
163                                         DBUS_TYPE_STRING, &iface,
164                                         DBUS_TYPE_STRING, &name,
165                                         DBUS_TYPE_INVALID))
166                 return NULL;
167 
168 	if (!spa_streq(iface, PLAYER_INTERFACE))
169 		return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
170 				"No such interface");
171 
172 	r = dbus_message_new_method_return(m);
173 	if (r == NULL)
174 		return NULL;
175 
176 	dbus_message_iter_init_append(r, &i);
177 	append_properties(impl, &i);
178 	return r;
179 }
180 
properties_set(struct impl * impl,DBusMessage * m)181 static DBusMessage *properties_set(struct impl *impl, DBusMessage *m)
182 {
183 	return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
184 			"Property not writable");
185 }
186 
introspect(struct impl * impl,DBusMessage * m)187 static DBusMessage *introspect(struct impl *impl, DBusMessage *m)
188 {
189 	const char *xml = PLAYER_INTROSPECT_XML;
190 	DBusMessage *r;
191 	if ((r = dbus_message_new_method_return(m)) == NULL)
192 		return NULL;
193 	if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
194 		return NULL;
195 	return r;
196 }
197 
player_handler(DBusConnection * c,DBusMessage * m,void * userdata)198 static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
199 {
200 	struct impl *impl = userdata;
201 	DBusMessage *r;
202 
203 	if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
204 		r = introspect(impl, m);
205 	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
206 		r = properties_get(impl, m);
207 	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
208 		r = properties_get_all(impl, m);
209 	} else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
210 		r = properties_set(impl, m);
211 	} else {
212 		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
213 	}
214 
215 	if (r == NULL)
216 		return DBUS_HANDLER_RESULT_NEED_MEMORY;
217 	if (!dbus_connection_send(impl->conn, r, NULL)) {
218 		dbus_message_unref(r);
219 		return DBUS_HANDLER_RESULT_NEED_MEMORY;
220 	}
221 	dbus_message_unref(r);
222 	return DBUS_HANDLER_RESULT_HANDLED;
223 }
224 
send_update_signal(struct impl * impl)225 static int send_update_signal(struct impl *impl)
226 {
227 	DBusMessage *m;
228 	const char *iface = PLAYER_INTERFACE;
229 	DBusMessageIter i, a;
230 	int res = 0;
231 
232 	m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged");
233 	if (m == NULL)
234 		return -ENOMEM;
235 
236 	dbus_message_iter_init_append(m, &i);
237 	dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface);
238 
239 	append_properties(impl, &i);
240 
241 	dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
242 			DBUS_TYPE_STRING_AS_STRING, &a);
243         dbus_message_iter_close_container(&i, &a);
244 
245 	if (!dbus_connection_send(impl->conn, m, NULL))
246 		res = -EIO;
247 
248 	dbus_message_unref(m);
249 
250 	return res;
251 }
252 
update_properties(struct impl * impl,bool send_signal)253 static void update_properties(struct impl *impl, bool send_signal)
254 {
255 	int nitems = 0;
256 
257 	switch (impl->this.state) {
258 	case SPA_BT_PLAYER_PLAYING:
259 		impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing");
260 		break;
261 	case SPA_BT_PLAYER_STOPPED:
262 		impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped");
263 		break;
264 	}
265 	impl->properties = SPA_DICT_INIT(impl->properties_items, nitems);
266 
267 	if (!send_signal)
268 		return;
269 
270 	send_update_signal(impl);
271 }
272 
spa_bt_player_new(void * dbus_connection,struct spa_log * log)273 struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log)
274 {
275 	struct impl *impl;
276 	const DBusObjectPathVTable vtable = {
277 		.message_function = player_handler,
278 	};
279 
280 	spa_log_topic_init(log, &log_topic);
281 
282 	impl = calloc(1, sizeof(struct impl));
283 	if (impl == NULL)
284 		return NULL;
285 
286 	impl->this.state = SPA_BT_PLAYER_STOPPED;
287 	impl->conn = dbus_connection;
288 	impl->log = log;
289 	impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++);
290 	if (impl->path == NULL) {
291 		free(impl);
292 		return NULL;
293 	}
294 
295 	dbus_connection_ref(impl->conn);
296 
297 	update_properties(impl, false);
298 
299 	if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) {
300 		spa_bt_player_destroy(&impl->this);
301 		errno = EIO;
302 		return NULL;
303 	}
304 
305 	return &impl->this;
306 }
307 
spa_bt_player_destroy(struct spa_bt_player * player)308 void spa_bt_player_destroy(struct spa_bt_player *player)
309 {
310 	struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
311 
312 	/*
313 	 * We unregister only the object path, but don't unregister it from
314 	 * BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is
315 	 * that the DBus connection is terminated immediately after.
316 	 */
317 	dbus_connection_unregister_object_path(impl->conn, impl->path);
318 
319 	dbus_connection_unref(impl->conn);
320 	free(impl->path);
321 	free(impl);
322 }
323 
spa_bt_player_set_state(struct spa_bt_player * player,enum spa_bt_player_state state)324 int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state)
325 {
326 	struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
327 
328 	switch (state) {
329 	case SPA_BT_PLAYER_PLAYING:
330 		if (impl->playing_count++ > 0)
331 			return 0;
332 		break;
333 	case SPA_BT_PLAYER_STOPPED:
334 		if (impl->playing_count == 0)
335 			return -EINVAL;
336 		if (--impl->playing_count > 0)
337 			return 0;
338 		break;
339 	default:
340 		return -EINVAL;
341 	}
342 
343 	impl->this.state = state;
344 	update_properties(impl, true);
345 
346 	return 0;
347 }
348 
spa_bt_player_register(struct spa_bt_player * player,const char * adapter_path)349 int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path)
350 {
351 	struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
352 
353 	DBusError err;
354 	DBusMessageIter i;
355 	DBusMessage *m, *r;
356 	int res = 0;
357 
358 	spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s",
359 			impl->path, adapter_path);
360 
361 	m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
362 			BLUEZ_MEDIA_INTERFACE, "RegisterPlayer");
363 	if (m == NULL)
364 		return -EIO;
365 
366 	dbus_message_iter_init_append(m, &i);
367 	dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
368 	append_properties(impl, &i);
369 
370 	dbus_error_init(&err);
371 	r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
372 	dbus_message_unref(m);
373 
374 	if (r == NULL) {
375 		spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
376 		dbus_error_free(&err);
377 		return -EIO;
378 	}
379 
380 	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
381 		spa_log_error(impl->log, "RegisterPlayer() failed");
382 		res = -EIO;
383 	}
384 
385 	dbus_message_unref(r);
386 
387 	return res;
388 }
389 
spa_bt_player_unregister(struct spa_bt_player * player,const char * adapter_path)390 int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
391 {
392 	struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
393 
394 	DBusError err;
395 	DBusMessageIter i;
396 	DBusMessage *m, *r;
397 	int res = 0;
398 
399 	spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s",
400 			impl->path, adapter_path);
401 
402 	m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
403 			BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer");
404 	if (m == NULL)
405 		return -EIO;
406 
407 	dbus_message_iter_init_append(m, &i);
408 	dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
409 
410 	dbus_error_init(&err);
411 	r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
412 	dbus_message_unref(m);
413 
414 	if (r == NULL) {
415 		spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
416 		dbus_error_free(&err);
417 		return -EIO;
418 	}
419 
420 	if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
421 		spa_log_error(impl->log, "UnregisterPlayer() failed");
422 		res = -EIO;
423 	}
424 
425 	dbus_message_unref(r);
426 
427 	return res;
428 }
429