hythmbox-2.98/plugins/daap/rb-dacp-pairing-page.c

No issues found

  1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
  2  *
  3  *  Implementation of DACP (iTunes Remote) pairing page object
  4  *
  5  *  Copyright (C) 2010 Alexandre Rosenfeld <alexandre.rosenfeld@gmail.com>
  6  *
  7  *  This program is free software; you can redistribute it and/or modify
  8  *  it under the terms of the GNU General Public License as published by
  9  *  the Free Software Foundation; either version 2 of the License, or
 10  *  (at your option) any later version.
 11  *
 12  *  The Rhythmbox authors hereby grant permission for non-GPL compatible
 13  *  GStreamer plugins to be used and distributed together with GStreamer
 14  *  and Rhythmbox. This permission is above and beyond the permissions granted
 15  *  by the GPL license by which Rhythmbox is covered. If you modify this code
 16  *  you may extend this exception to your version of the code, but you are not
 17  *  obligated to do so. If you do not wish to do so, delete this exception
 18  *  statement from your version.
 19  *
 20  *  This program is distributed in the hope that it will be useful,
 21  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 22  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 23  *  GNU General Public License for more details.
 24  *
 25  *  You should have received a copy of the GNU General Public License
 26  *  along with this program; if not, write to the Free Software
 27  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
 28  *
 29  */
 30 
 31 #include "config.h"
 32 
 33 #include <string.h>
 34 #include <ctype.h>
 35 #include <math.h>
 36 
 37 #include <glib/gi18n.h>
 38 #include <gtk/gtk.h>
 39 
 40 #include "rhythmdb.h"
 41 #include "rb-shell.h"
 42 #include "rb-display-page-group.h"
 43 #include "rb-stock-icons.h"
 44 #include "rb-debug.h"
 45 #include "rb-util.h"
 46 #include "rb-file-helpers.h"
 47 #include "rb-builder-helpers.h"
 48 #include "rb-dialog.h"
 49 #include "rb-playlist-manager.h"
 50 #include "rb-shell-player.h"
 51 #include "rb-display-page-model.h"
 52 #include "rb-rhythmdb-dmap-db-adapter.h"
 53 #include "rb-dmap-container-db-adapter.h"
 54 
 55 #include "rb-daap-plugin.h"
 56 #include "rb-daap-sharing.h"
 57 #include "rb-dacp-player.h"
 58 
 59 #include <libdmapsharing/dmap.h>
 60 
 61 #include "rb-dacp-pairing-page.h"
 62 
 63 static void impl_constructed (GObject *object);
 64 static void impl_dispose (GObject *object);
 65 static void impl_set_property  (GObject *object,
 66 				guint prop_id,
 67 				const GValue *value,
 68 				GParamSpec *pspec);
 69 static void impl_get_property  (GObject *object,
 70 				guint prop_id,
 71 				GValue *value,
 72 				GParamSpec *pspec);
 73 
 74 static void rb_dacp_pairing_page_connecting (RBDACPPairingPage *page, gboolean connecting);
 75 static gboolean entry_insert_text_cb (GtkWidget *entry, gchar *text, gint len, gint *position, RBDACPPairingPage *page);
 76 static gboolean entry_backspace_cb (GtkWidget *entry, RBDACPPairingPage *page);
 77 static void remote_paired_cb (DACPShare *share, gchar *service_name, gboolean connected, RBDACPPairingPage *page);
 78 
 79 static void dacp_remote_added (DACPShare *share, gchar *service_name, gchar *display_name, RBDaapPlugin *plugin);
 80 static void dacp_remote_removed (DACPShare *share, gchar *service_name, RBDaapPlugin *plugin);
 81 
 82 /* DACPShare signals */
 83 static gboolean dacp_lookup_guid (DACPShare *share, gchar *guid, GSettings *settings);
 84 static void     dacp_add_guid    (DACPShare *share, gchar *guid, GSettings *settings);
 85 
 86 static void dacp_player_updated (RBDACPPlayer *player, DACPShare *share);
 87 
 88 struct RBDACPPairingPagePrivate
 89 {
 90 	char *service_name;
 91 
 92 	gboolean done_pairing;
 93 
 94 	DACPShare *dacp_share;
 95 
 96 	GtkBuilder *builder;
 97 	GtkWidget *entries[4];
 98 	GtkWidget *finished_widget;
 99 	GtkWidget *pairing_widget;
100 	GtkWidget *pairing_status_widget;
101 };
102 
103 enum {
104 	PROP_0,
105 	PROP_SERVICE_NAME
106 };
107 
108 G_DEFINE_DYNAMIC_TYPE (RBDACPPairingPage, rb_dacp_pairing_page, RB_TYPE_DISPLAY_PAGE)
109 
110 static gboolean
111 entry_insert_text_cb (GtkWidget *entry, gchar *text, gint len, gint *position, RBDACPPairingPage *page)
112 {
113 	gchar new_char = text[*position];
114 	gint entry_pos = 0;
115 	gchar passcode[4];
116 	int i;
117 
118 	/* Find out which entry the user just entered text */
119 	for (entry_pos = 0; entry_pos < 4; entry_pos++) {
120 		if (entry == page->priv->entries[entry_pos]) {
121 			break;
122 		}
123 	}
124 
125 	if (!isdigit (new_char)) {
126 		/* is this a number? If not, don't let it in */
127 		g_signal_stop_emission_by_name (entry, "insert-text");
128 		return TRUE;
129 	}
130 	if (entry_pos < 3) {
131 		/* Focus the next entry */
132 		gtk_widget_grab_focus (page->priv->entries[entry_pos + 1]);
133 	} else if (entry_pos == 3) {
134 		/* The user entered all 4 characters of the passcode, so let's pair */
135 		for (i = 0; i < 3; i++) {
136 			const gchar *text = gtk_entry_get_text (GTK_ENTRY (page->priv->entries[i]));
137 			passcode[i] = text[0];
138 		}
139 		/* The last character is still not in the entry */
140 		passcode[3] = new_char;
141 		rb_dacp_pairing_page_connecting (page, TRUE);
142 		/* Let DACPShare do the heavy-lifting */
143 		dacp_share_pair (page->priv->dacp_share,
144 		                 page->priv->service_name,
145 		                 passcode);
146 	}
147 	/* let the default handler display the number */
148 	return FALSE;
149 }
150 
151 static gboolean
152 entry_backspace_cb (GtkWidget *entry, RBDACPPairingPage *page)
153 {
154 	gint entry_pos = 0;
155 
156 	/* Find out which entry the user just entered text */
157 	for (entry_pos = 0; entry_pos < 4; entry_pos++) {
158 		if (entry == page->priv->entries[entry_pos]) {
159 			break;
160 		}
161 	}
162 
163 	if (entry_pos > 0) {
164 		gtk_entry_set_text (GTK_ENTRY (page->priv->entries[entry_pos]), "");
165 		/* Focus the previous entry */
166 		gtk_widget_grab_focus (page->priv->entries[entry_pos - 1]);
167 	}
168 
169 	return FALSE;
170 }
171 
172 static gboolean
173 close_pairing_clicked_cb (GtkButton *button, RBDACPPairingPage *page)
174 {
175 	rb_display_page_delete_thyself (RB_DISPLAY_PAGE (page));
176 	return FALSE;
177 }
178 
179 static void
180 impl_dispose (GObject *object)
181 {
182 	RBDACPPairingPage *page = RB_DACP_PAIRING_PAGE (object);
183 
184 	if (page->priv->builder != NULL) {
185 		g_object_unref (page->priv->builder);
186 		page->priv->builder = NULL;
187 	}
188 
189 	if (page->priv->dacp_share != NULL) {
190 		g_object_unref (page->priv->dacp_share);
191 		page->priv->dacp_share = NULL;
192 	}
193 
194 	G_OBJECT_CLASS (rb_dacp_pairing_page_parent_class)->dispose (object);
195 }
196 
197 static void
198 impl_finalize (GObject *object)
199 {
200 	RBDACPPairingPage *page = RB_DACP_PAIRING_PAGE (object);
201 
202 	g_free (page->priv->service_name);
203 
204 	G_OBJECT_CLASS (rb_dacp_pairing_page_parent_class)->finalize (object);
205 }
206 
207 static void
208 rb_dacp_pairing_page_class_init (RBDACPPairingPageClass *klass)
209 {
210 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
211 
212 	object_class->constructed  = impl_constructed;
213 	object_class->dispose      = impl_dispose;
214 	object_class->finalize     = impl_finalize;
215 	object_class->get_property = impl_get_property;
216 	object_class->set_property = impl_set_property;
217 
218 	g_object_class_install_property (object_class,
219 					 PROP_SERVICE_NAME,
220 					 g_param_spec_string ("service-name",
221 							      "Service name",
222 							      "mDNS/DNS-SD service name of the share",
223 							      NULL,
224 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
225 
226 	g_type_class_add_private (klass, sizeof (RBDACPPairingPagePrivate));
227 }
228 
229 static void
230 rb_dacp_pairing_page_class_finalize (RBDACPPairingPageClass *klass)
231 {
232 }
233 
234 static void
235 rb_dacp_pairing_page_init (RBDACPPairingPage *page)
236 {
237 	page->priv = G_TYPE_INSTANCE_GET_PRIVATE (page,
238 						  RB_TYPE_DACP_PAIRING_PAGE,
239 						  RBDACPPairingPagePrivate);
240 }
241 
242 static void
243 impl_constructed (GObject *object)
244 {
245 	RBDACPPairingPage *page = RB_DACP_PAIRING_PAGE (object);
246 	char *builder_filename;
247 	GtkWidget *passcode_widget;
248 	GtkWidget *close_pairing_button;
249 	PangoFontDescription *font;
250 	GObject *plugin;
251 	int i;
252 
253 	g_object_get (page, "plugin", &plugin, NULL);
254 
255 	builder_filename = rb_find_plugin_data_file (G_OBJECT (plugin), "daap-prefs.ui");
256 	g_assert (builder_filename != NULL);
257 
258 	page->priv->builder = rb_builder_load (builder_filename, NULL);
259 	g_free (builder_filename);
260 
261 	passcode_widget = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, "passcode_widget"));
262 	gtk_container_add (GTK_CONTAINER (page), passcode_widget);
263 
264 	close_pairing_button = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, "close_pairing_button"));
265 	g_signal_connect_object (close_pairing_button, "clicked", G_CALLBACK (close_pairing_clicked_cb), page, 0);
266 
267 	page->priv->finished_widget = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, "finished_widget"));
268 	page->priv->pairing_widget = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, "pairing_widget"));
269 	page->priv->pairing_status_widget = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, "pairing_status_widget"));
270 
271 	font = pango_font_description_from_string ("normal 28");
272 
273 	for (i = 0; i < 4; i++) {
274 		char *entry_name;
275 
276 		entry_name = g_strdup_printf ("passcode_entry%d", i + 1);
277 		page->priv->entries[i] = GTK_WIDGET (gtk_builder_get_object (page->priv->builder, entry_name));
278 		gtk_widget_override_font (page->priv->entries[i], font);
279 		g_signal_connect_object (page->priv->entries[i],
280 		                         "insert-text",
281 		                         G_CALLBACK (entry_insert_text_cb),
282 		                         page,
283 		                         0);
284 		g_signal_connect_object (page->priv->entries[i],
285 		                         "backspace",
286 		                         G_CALLBACK (entry_backspace_cb),
287 		                         page,
288 		                         0);
289 		g_free (entry_name);
290 	}
291 
292 	pango_font_description_free (font);
293 
294 	gtk_widget_show (passcode_widget);
295 
296 	g_object_unref (plugin);
297 }
298 
299 static void
300 impl_set_property (GObject *object,
301 		   guint prop_id,
302 		   const GValue *value,
303 		   GParamSpec *pspec)
304 {
305 	RBDACPPairingPage *page = RB_DACP_PAIRING_PAGE (object);
306 
307 	switch (prop_id) {
308 	case PROP_SERVICE_NAME:
309 		page->priv->service_name = g_value_dup_string (value);
310 		break;
311 	default:
312 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
313 		break;
314 	}
315 }
316 
317 static void
318 impl_get_property (GObject *object,
319 		   guint prop_id,
320 		   GValue *value,
321 		   GParamSpec *pspec)
322 {
323 	RBDACPPairingPage *page = RB_DACP_PAIRING_PAGE (object);
324 
325 	switch (prop_id) {
326 	case PROP_SERVICE_NAME:
327 		g_value_set_string (value, page->priv->service_name);
328 		break;
329 	default:
330 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
331 		break;
332 	}
333 }
334 
335 RBDACPPairingPage *
336 rb_dacp_pairing_page_new (GObject *plugin,
337 			  RBShell *shell,
338 			  DACPShare *dacp_share,
339 			  const char *display_name,
340 			  const char *service_name)
341 {
342 	RBDACPPairingPage *page;
343 	char *icon_filename;
344 	int icon_size;
345 	GdkPixbuf *icon_pixbuf;
346 
347 	icon_filename = rb_find_plugin_data_file (plugin, "remote-icon.png");
348 	gtk_icon_size_lookup (GTK_ICON_SIZE_LARGE_TOOLBAR, &icon_size, NULL);
349 	icon_pixbuf = gdk_pixbuf_new_from_file_at_size (icon_filename, icon_size, icon_size, NULL);
350 
351 	page = RB_DACP_PAIRING_PAGE (g_object_new (RB_TYPE_DACP_PAIRING_PAGE,
352 						   "name", display_name,
353 						   "service-name", service_name,
354 						   "pixbuf", icon_pixbuf,
355 						   "shell", shell,
356 						   "plugin", plugin,
357 						   NULL));
358 
359 	g_object_ref (dacp_share);
360 	page->priv->dacp_share = dacp_share;
361 	/* Retrieve notifications when the remote is finished pairing */
362 	g_signal_connect_object (dacp_share, "remote-paired", G_CALLBACK (remote_paired_cb), page, 0);
363 
364 	g_free (icon_filename);
365 	g_object_unref (icon_pixbuf);
366 
367 	return page;
368 }
369 
370 
371 static void
372 rb_dacp_pairing_page_reset_passcode (RBDACPPairingPage *page)
373 {
374 	int i;
375 
376 	for (i = 0; i < 4; i++) {
377 		gtk_entry_set_text (GTK_ENTRY (page->priv->entries[i]), "");
378 	}
379 	gtk_widget_grab_focus (page->priv->entries [0]);
380 }
381 
382 void
383 rb_dacp_pairing_page_remote_found (RBDACPPairingPage *page)
384 {
385 	if (page->priv->done_pairing) {
386 		rb_dacp_pairing_page_reset_passcode (page);
387 		gtk_widget_show (page->priv->pairing_widget);
388 		gtk_widget_hide (page->priv->pairing_status_widget);
389 		gtk_widget_hide (page->priv->finished_widget);
390 		page->priv->done_pairing = FALSE;
391 	}
392 }
393 
394 void
395 rb_dacp_pairing_page_remote_lost (RBDACPPairingPage *page)
396 {
397 	if (!page->priv->done_pairing) {
398 		rb_display_page_delete_thyself (RB_DISPLAY_PAGE (page));
399 	}
400 }
401 
402 static void
403 rb_dacp_pairing_page_connecting (RBDACPPairingPage *page, gboolean connecting) {
404 	int i;
405 
406 	if (connecting) {
407 		gtk_widget_show (page->priv->pairing_status_widget);
408 		gtk_label_set_markup (GTK_LABEL (page->priv->pairing_status_widget), _("Connecting..."));
409 	} else {
410 		gtk_label_set_markup (GTK_LABEL (page->priv->pairing_status_widget), _("Could not pair with this Remote."));
411 	}
412 
413 	for (i = 0; i < 4; i++) {
414 		gtk_widget_set_sensitive (page->priv->entries [i], !connecting);
415 	}
416 }
417 
418 static void
419 remote_paired_cb (DACPShare *share, gchar *service_name, gboolean connected, RBDACPPairingPage *page)
420 {
421 	/* Check if this remote is the remote paired */
422 	if (g_strcmp0 (service_name, page->priv->service_name) != 0)
423 		return;
424 
425 	rb_dacp_pairing_page_connecting (page, FALSE);
426 	if (connected) {
427 		gtk_widget_hide (page->priv->pairing_widget);
428 		gtk_widget_show (page->priv->finished_widget);
429 		page->priv->done_pairing = TRUE;
430 	} else {
431 		gtk_widget_show (page->priv->pairing_status_widget);
432 		rb_dacp_pairing_page_reset_passcode (page);
433 	}
434 }
435 
436 DACPShare *
437 rb_daap_create_dacp_share (GObject *plugin)
438 {
439 	DACPShare *share;
440 	DACPPlayer *player;
441 	RhythmDB *rdb;
442 	DMAPDb *db;
443 	DMAPContainerDb *container_db;
444 	RBPlaylistManager *playlist_manager;
445 	RBShell *shell;
446 	GSettings *share_settings;
447 	GSettings *daap_settings;
448 	GSettings *settings;
449 	gchar *name;
450 
451 	g_object_get (plugin, "object", &shell, NULL);
452 
453 	g_object_get (shell,
454 	              "db", &rdb,
455 	              "playlist-manager", &playlist_manager,
456 	              NULL);
457 	db = DMAP_DB (rb_rhythmdb_dmap_db_adapter_new (rdb, RHYTHMDB_ENTRY_TYPE_SONG));
458 	container_db = DMAP_CONTAINER_DB (rb_dmap_container_db_adapter_new (playlist_manager));
459 
460 	player = DACP_PLAYER (rb_dacp_player_new (shell));
461 
462 	share_settings = g_settings_new ("org.gnome.rhythmbox.sharing");
463 	name = g_settings_get_string (share_settings, "share-name");
464 	if (name == NULL || *name == '\0') {
465 		g_free (name);
466 		name = rb_daap_sharing_default_share_name ();
467 	}
468 	g_object_unref (share_settings);
469 
470 	share = dacp_share_new (name, player, db, container_db);
471 
472 	daap_settings = g_settings_new ("org.gnome.rhythmbox.plugins.daap");
473 	settings = g_settings_get_child (daap_settings, "dacp");
474 	g_object_unref (daap_settings);
475 
476 	g_signal_connect_object (share,
477 				 "add-guid",
478 				 G_CALLBACK (dacp_add_guid),
479 				 settings,
480 				 0);
481 	g_signal_connect_object (share,
482 				 "lookup-guid",
483 				 G_CALLBACK (dacp_lookup_guid),
484 				 settings,
485 				 0);
486 
487 	g_signal_connect_object (share,
488 				 "remote-found",
489 				 G_CALLBACK (dacp_remote_added),
490 				 RB_DAAP_PLUGIN (plugin),
491 				 0);
492 	g_signal_connect_object (share,
493 				 "remote-lost",
494 				 G_CALLBACK (dacp_remote_removed),
495 				 RB_DAAP_PLUGIN (plugin),
496 				 0);
497 
498 	g_signal_connect_object (player,
499 	                         "player-updated",
500 	                         G_CALLBACK (dacp_player_updated),
501 	                         share,
502 	                         0);
503 
504 	g_object_unref (db);
505 	g_object_unref (container_db);
506 	g_object_unref (rdb);
507 	g_object_unref (playlist_manager);
508 	g_object_unref (player);
509 	g_object_unref (shell);
510 
511 	return share;
512 }
513 
514 static void
515 dacp_player_updated (RBDACPPlayer *player,
516                      DACPShare *share)
517 {
518 	dacp_share_player_updated (share);
519 }
520 
521 static void
522 dacp_add_guid (DACPShare *share,
523                gchar *guid,
524 	       GSettings *settings)
525 {
526 	GVariantBuilder *vb;
527 	GVariantIter iter;
528 	GVariant *v;
529 	const char *g;
530 
531 	v = g_settings_get_value (settings, "known-remotes");
532 
533 	vb = g_variant_builder_new (G_VARIANT_TYPE ("as"));
534 	g_variant_iter_init (&iter, v);
535 	while (g_variant_iter_loop (&iter, "s", &g)) {
536 		g_variant_builder_add (vb, "s", g);
537 	}
538 
539 	g_variant_builder_add (vb, "s", guid);
540 	g_variant_unref (v);
541 
542 	g_settings_set_value (settings, "known-remotes", g_variant_builder_end (vb));
543 	g_variant_builder_unref (vb);
544 }
545 
546 static gboolean
547 dacp_lookup_guid (DACPShare *share,
548                   gchar *guid,
549 		  GSettings *settings)
550 {
551 	char **guids;
552 	gboolean found;
553 
554 	guids = g_settings_get_strv (settings, "known-remotes");
555 	found = rb_str_in_strv (guid, (const char **)guids);
556 	g_strfreev (guids);
557 
558 	return found;
559 }
560 
561 typedef struct {
562 	const char *name;
563 	RBDACPPairingPage *page;
564 } FindPage;
565 
566 static gboolean
567 find_dacp_page_foreach (GtkTreeModel *model,
568                         GtkTreePath  *path,
569                         GtkTreeIter  *iter,
570                         FindPage     *fp)
571 {
572 	gchar *name;
573 	RBDisplayPage *page;
574 
575 	gtk_tree_model_get (model, iter,
576 	                    RB_DISPLAY_PAGE_MODEL_COLUMN_PAGE, &page,
577 	                    -1);
578 	if (page && RB_IS_DACP_PAIRING_PAGE (page)) {
579 		g_object_get (page, "service-name", &name, NULL);
580 		if (strcmp (name, fp->name) == 0) {
581 			fp->page = RB_DACP_PAIRING_PAGE (page);
582 		}
583 		g_free (name);
584 	}
585 
586 	return (fp->page != NULL);
587 }
588 
589 static RBDACPPairingPage *
590 find_dacp_page (RBShell *shell, const gchar *service_name)
591 {
592 	RBDisplayPageModel *page_model;
593 	FindPage find_page;
594 
595 	find_page.name = service_name;
596 	find_page.page = NULL;
597 
598 	g_object_get (shell, "display-page-model", &page_model, NULL);
599 
600 	gtk_tree_model_foreach (GTK_TREE_MODEL (page_model),
601 	                        (GtkTreeModelForeachFunc) find_dacp_page_foreach,
602 	                        &find_page);
603 
604 	return find_page.page;
605 }
606 
607 static void
608 dacp_remote_added (DACPShare    *share,
609                    gchar        *service_name,
610                    gchar        *display_name,
611                    RBDaapPlugin *plugin)
612 {
613 	RBDACPPairingPage *page;
614 	RBShell *shell;
615 
616 	rb_debug ("Remote %s (%s) found", service_name, display_name);
617 
618 	g_object_get (plugin, "object", &shell, NULL);
619 
620 	GDK_THREADS_ENTER ();
621 
622 	page = find_dacp_page (shell, service_name);
623 	if (page == NULL) {
624 		RBDisplayPageGroup *page_group;
625 
626 		page_group = rb_display_page_group_get_by_id ("remotes");
627 		if (page_group == NULL) {
628 			page_group = rb_display_page_group_new (G_OBJECT (shell),
629 								"remotes",
630 								_("Remotes"),
631 								RB_DISPLAY_PAGE_GROUP_CATEGORY_TRANSIENT);
632 			rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (page_group), NULL);
633 		}
634 
635 		page = rb_dacp_pairing_page_new (G_OBJECT (plugin), shell, share, display_name, service_name);
636 
637 		rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (page), RB_DISPLAY_PAGE (page_group));
638 	} else {
639 		rb_dacp_pairing_page_remote_found (page);
640 	}
641 
642 	GDK_THREADS_LEAVE ();
643 
644 	g_object_unref (shell);
645 }
646 
647 static void
648 dacp_remote_removed (DACPShare       *share,
649                      gchar           *service_name,
650                      RBDaapPlugin    *plugin)
651 {
652 	RBDACPPairingPage *page;
653 	RBShell *shell;
654 
655 	rb_debug ("Remote '%s' went away", service_name);
656 
657 	g_object_get (plugin, "object", &shell, NULL);
658 
659 	GDK_THREADS_ENTER ();
660 
661 	page = find_dacp_page (shell, service_name);
662 	if (page != NULL) {
663 		rb_dacp_pairing_page_remote_lost (page);
664 	}
665 
666 	GDK_THREADS_LEAVE ();
667 
668 	g_object_unref (shell);
669 }
670 
671 void
672 _rb_dacp_pairing_page_register_type (GTypeModule *module)
673 {
674 	rb_dacp_pairing_page_register_type (module);
675 }