123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gio = imports.gi.Gio;
- const GLib = imports.gi.GLib;
- const GObject = imports.gi.GObject;
- const Config = imports.config;
- const DBus = imports.service.utils.dbus;
- const Device = imports.service.device;
- const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device';
- const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device';
- const DEVICE_IFACE = Config.DBUS.lookup_interface(DEVICE_NAME);
- /**
- * A manager for devices.
- */
- var Manager = GObject.registerClass({
- GTypeName: 'GSConnectManager',
- Properties: {
- 'active': GObject.ParamSpec.boolean(
- 'active',
- 'Active',
- 'Whether the manager is active',
- GObject.ParamFlags.READABLE,
- false
- ),
- 'discoverable': GObject.ParamSpec.boolean(
- 'discoverable',
- 'Discoverable',
- 'Whether the service responds to discovery requests',
- GObject.ParamFlags.READWRITE,
- false
- ),
- 'id': GObject.ParamSpec.string(
- 'id',
- 'Id',
- 'The hostname or other network unique id',
- GObject.ParamFlags.READWRITE,
- null
- ),
- 'name': GObject.ParamSpec.string(
- 'name',
- 'Name',
- 'The name announced to the network',
- GObject.ParamFlags.READWRITE,
- 'GSConnect'
- ),
- },
- }, class Manager extends Gio.DBusObjectManagerServer {
- _init(params = {}) {
- super._init(params);
- this._exported = new WeakMap();
- this._reconnectId = 0;
- this._settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true),
- });
- this._initSettings();
- }
- get active() {
- if (this._active === undefined)
- this._active = false;
- return this._active;
- }
- get backends() {
- if (this._backends === undefined)
- this._backends = new Map();
- return this._backends;
- }
- get devices() {
- if (this._devices === undefined)
- this._devices = new Map();
- return this._devices;
- }
- get discoverable() {
- if (this._discoverable === undefined)
- this._discoverable = this.settings.get_boolean('discoverable');
- return this._discoverable;
- }
- set discoverable(value) {
- if (this.discoverable === value)
- return;
- this._discoverable = value;
- this.notify('discoverable');
- // FIXME: This whole thing just keeps getting uglier
- const application = Gio.Application.get_default();
- if (application === null)
- return;
- if (this.discoverable) {
- Gio.Application.prototype.withdraw_notification.call(
- application,
- 'discovery-warning'
- );
- } else {
- const notif = new Gio.Notification();
- notif.set_title(_('Discovery Disabled'));
- notif.set_body(_('Discovery has been disabled due to the number of devices on this network.'));
- notif.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'}));
- notif.set_priority(Gio.NotificationPriority.HIGH);
- notif.set_default_action('app.preferences');
- Gio.Application.prototype.withdraw_notification.call(
- application,
- 'discovery-warning',
- notif
- );
- }
- }
- get id() {
- if (this._id === undefined)
- this._id = this.settings.get_string('id');
- return this._id;
- }
- set id(value) {
- if (this.id === value)
- return;
- this._id = value;
- this.notify('id');
- }
- get name() {
- if (this._name === undefined)
- this._name = this.settings.get_string('name');
- return this._name;
- }
- set name(value) {
- if (this.name === value)
- return;
- this._name = value;
- this.notify('name');
- // Broadcast changes to the network
- for (const backend of this.backends.values()) {
- backend.name = this.name;
- backend.buildIdentity();
- }
- this.identify();
- }
- get settings() {
- if (this._settings === undefined) {
- this._settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true),
- });
- }
- return this._settings;
- }
- vfunc_notify(pspec) {
- if (pspec.name !== 'connection')
- return;
- if (this.connection !== null)
- this._exportDevices();
- else
- this._unexportDevices();
- }
- /*
- * GSettings
- */
- _initSettings() {
- // Initialize the ID and name of the service
- if (this.settings.get_string('id').length === 0)
- this.settings.set_string('id', GLib.uuid_string_random());
- if (this.settings.get_string('name').length === 0)
- this.settings.set_string('name', GLib.get_host_name());
- // Bound Properties
- this.settings.bind('discoverable', this, 'discoverable', 0);
- this.settings.bind('id', this, 'id', 0);
- this.settings.bind('name', this, 'name', 0);
- }
- /*
- * Backends
- */
- _onChannel(backend, channel) {
- try {
- let device = this.devices.get(channel.identity.body.deviceId);
- switch (true) {
- // Proceed if this is an existing device...
- case (device !== undefined):
- break;
- // Or the connection is allowed...
- case this.discoverable || channel.allowed:
- device = this._ensureDevice(channel.identity);
- break;
- // ...otherwise bail
- default:
- debug(`${channel.identity.body.deviceName}: not allowed`);
- return false;
- }
- device.setChannel(channel);
- return true;
- } catch (e) {
- logError(e, backend.name);
- return false;
- }
- }
- _loadBackends() {
- for (const name in imports.service.backends) {
- try {
- const module = imports.service.backends[name];
- if (module.ChannelService === undefined)
- continue;
- // Try to create the backend and track it if successful
- const backend = new module.ChannelService({
- id: this.id,
- name: this.name,
- });
- this.backends.set(name, backend);
- // Connect to the backend
- backend.__channelId = backend.connect(
- 'channel',
- this._onChannel.bind(this)
- );
- // Now try to start the backend, allowing us to retry if we fail
- backend.start();
- } catch (e) {
- if (Gio.Application.get_default())
- Gio.Application.get_default().notify_error(e);
- }
- }
- }
- /*
- * Devices
- */
- _loadDevices() {
- // Load cached devices
- for (const id of this.settings.get_strv('devices')) {
- const device = new Device.Device({body: {deviceId: id}});
- this._exportDevice(device);
- this.devices.set(id, device);
- }
- }
- _exportDevice(device) {
- if (this.connection === null)
- return;
- const info = {
- object: null,
- interface: null,
- actions: 0,
- menu: 0,
- };
- const objectPath = `${DEVICE_PATH}/${device.id.replace(/\W+/g, '_')}`;
- // Export an object path for the device
- info.object = new Gio.DBusObjectSkeleton({
- g_object_path: objectPath,
- });
- this.export(info.object);
- // Export GActions & GMenu
- info.actions = Gio.DBus.session.export_action_group(objectPath, device);
- info.menu = Gio.DBus.session.export_menu_model(objectPath, device.menu);
- // Export the Device interface
- info.interface = new DBus.Interface({
- g_instance: device,
- g_interface_info: DEVICE_IFACE,
- });
- info.object.add_interface(info.interface);
- this._exported.set(device, info);
- }
- _exportDevices() {
- if (this.connection === null)
- return;
- for (const device of this.devices.values())
- this._exportDevice(device);
- }
- _unexportDevice(device) {
- const info = this._exported.get(device);
- if (info === undefined)
- return;
- // Unexport GActions and GMenu
- Gio.DBus.session.unexport_action_group(info.actions);
- Gio.DBus.session.unexport_menu_model(info.menu);
- // Unexport the Device interface and object
- info.interface.flush();
- info.object.remove_interface(info.interface);
- info.object.flush();
- this.unexport(info.object.g_object_path);
- this._exported.delete(device);
- }
- _unexportDevices() {
- for (const device of this.devices.values())
- this._unexportDevice(device);
- }
- /**
- * Return a device for @packet, creating it and adding it to the list of
- * of known devices if it doesn't exist.
- *
- * @param {Core.Packet} packet - An identity packet for the device
- * @return {Device.Device} A device object
- */
- _ensureDevice(packet) {
- let device = this.devices.get(packet.body.deviceId);
- if (device === undefined) {
- debug(`Adding ${packet.body.deviceName}`);
- // TODO: Remove when all clients support bluetooth-like discovery
- //
- // If this is the third unpaired device to connect, we disable
- // discovery to avoid choking on networks with many devices
- const unpaired = Array.from(this.devices.values()).filter(dev => {
- return !dev.paired;
- });
- if (unpaired.length === 3)
- this.discoverable = false;
- device = new Device.Device(packet);
- this._exportDevice(device);
- this.devices.set(device.id, device);
- // Notify
- this.settings.set_strv('devices', Array.from(this.devices.keys()));
- }
- return device;
- }
- /**
- * Permanently remove a device.
- *
- * Removes the device from the list of known devices, deletes all GSettings
- * and files.
- *
- * @param {string} id - The id of the device to delete
- */
- _removeDevice(id) {
- // Delete all GSettings
- const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`;
- GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`);
- // Delete the cache
- const cache = GLib.build_filenamev([Config.CACHEDIR, id]);
- Gio.File.rm_rf(cache);
- // Forget the device
- this.devices.delete(id);
- this.settings.set_strv('devices', Array.from(this.devices.keys()));
- }
- /**
- * A GSourceFunc that tries to reconnect to each paired device, while
- * pruning unpaired devices that have disconnected.
- *
- * @return {boolean} Always %true
- */
- _reconnect() {
- for (const [id, device] of this.devices) {
- if (device.connected)
- continue;
- if (device.paired) {
- this.identify(device.settings.get_string('last-connection'));
- continue;
- }
- this._unexportDevice(device);
- this._removeDevice(id);
- device.destroy();
- }
- return GLib.SOURCE_CONTINUE;
- }
- /**
- * Identify to an address or broadcast to the network.
- *
- * @param {string} [uri] - An address URI or %null to broadcast
- */
- identify(uri = null) {
- try {
- // If we're passed a parameter, try and find a backend for it
- if (uri !== null) {
- const [scheme, address] = uri.split('://');
- const backend = this.backends.get(scheme);
- if (backend !== undefined)
- backend.broadcast(address);
- // If we're not discoverable, only try to reconnect known devices
- } else if (!this.discoverable) {
- this._reconnect();
- // Otherwise have each backend broadcast to it's network
- } else {
- this.backends.forEach(backend => backend.broadcast());
- }
- } catch (e) {
- logError(e);
- }
- }
- /**
- * Start managing devices.
- */
- start() {
- if (this.active)
- return;
- this._loadDevices();
- this._loadBackends();
- if (this._reconnectId === 0) {
- this._reconnectId = GLib.timeout_add_seconds(
- GLib.PRIORITY_LOW,
- 5,
- this._reconnect.bind(this)
- );
- }
- this._active = true;
- this.notify('active');
- }
- /**
- * Stop managing devices.
- */
- stop() {
- if (!this.active)
- return;
- if (this._reconnectId > 0) {
- GLib.Source.remove(this._reconnectId);
- this._reconnectId = 0;
- }
- this._unexportDevices();
- this.backends.forEach(backend => backend.destroy());
- this.backends.clear();
- this.devices.forEach(device => device.destroy());
- this.devices.clear();
- this._active = false;
- this.notify('active');
- }
- /**
- * Stop managing devices and free any resources.
- */
- destroy() {
- this.stop();
- this.set_connection(null);
- }
- });
|