123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- // 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 SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect';
- const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect';
- const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device';
- const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device';
- const _PROPERTIES = {
- 'Connected': 'connected',
- 'EncryptionInfo': 'encryption-info',
- 'IconName': 'icon-name',
- 'Id': 'id',
- 'Name': 'name',
- 'Paired': 'paired',
- 'Type': 'type',
- };
- function _proxyInit(proxy, cancellable = null) {
- if (proxy.__initialized !== undefined)
- return Promise.resolve();
- return new Promise((resolve, reject) => {
- proxy.init_async(
- GLib.PRIORITY_DEFAULT,
- cancellable,
- (proxy, res) => {
- try {
- proxy.init_finish(res);
- proxy.__initialized = true;
- resolve();
- } catch (e) {
- Gio.DBusError.strip_remote_error(e);
- reject(e);
- }
- }
- );
- });
- }
- /**
- * A simple proxy wrapper for devices exported over DBus.
- */
- var Device = GObject.registerClass({
- GTypeName: 'GSConnectRemoteDevice',
- Implements: [Gio.DBusInterface],
- Properties: {
- 'connected': GObject.ParamSpec.boolean(
- 'connected',
- 'Connected',
- 'Whether the device is connected',
- GObject.ParamFlags.READABLE,
- null
- ),
- 'encryption-info': GObject.ParamSpec.string(
- 'encryption-info',
- 'Encryption Info',
- 'A formatted string with the local and remote fingerprints',
- GObject.ParamFlags.READABLE,
- null
- ),
- 'icon-name': GObject.ParamSpec.string(
- 'icon-name',
- 'Icon Name',
- 'Icon name representing the device',
- GObject.ParamFlags.READABLE,
- null
- ),
- 'id': GObject.ParamSpec.string(
- 'id',
- 'deviceId',
- 'The device hostname or other unique id',
- GObject.ParamFlags.READABLE,
- ''
- ),
- 'name': GObject.ParamSpec.string(
- 'name',
- 'deviceName',
- 'The device name',
- GObject.ParamFlags.READABLE,
- null
- ),
- 'paired': GObject.ParamSpec.boolean(
- 'paired',
- 'Paired',
- 'Whether the device is paired',
- GObject.ParamFlags.READABLE,
- null
- ),
- 'type': GObject.ParamSpec.string(
- 'type',
- 'deviceType',
- 'The device type',
- GObject.ParamFlags.READABLE,
- null
- ),
- },
- }, class Device extends Gio.DBusProxy {
- _init(service, object_path) {
- this._service = service;
- super._init({
- g_connection: service.g_connection,
- g_name: SERVICE_NAME,
- g_object_path: object_path,
- g_interface_name: DEVICE_NAME,
- });
- }
- vfunc_g_properties_changed(changed, invalidated) {
- try {
- for (const name in changed.deepUnpack())
- this.notify(_PROPERTIES[name]);
- } catch (e) {
- logError(e);
- }
- }
- _get(name, fallback = null) {
- try {
- return this.get_cached_property(name).unpack();
- } catch (e) {
- return fallback;
- }
- }
- get connected() {
- return this._get('Connected', false);
- }
- get encryption_info() {
- return this._get('EncryptionInfo', '');
- }
- get icon_name() {
- return this._get('IconName', 'computer');
- }
- get id() {
- return this._get('Id', '0');
- }
- get name() {
- return this._get('Name', 'Unknown');
- }
- get paired() {
- return this._get('Paired', false);
- }
- get service() {
- return this._service;
- }
- get type() {
- return this._get('Type', 'desktop');
- }
- async start() {
- try {
- await _proxyInit(this);
- // For GActions & GMenu we pass the service's name owner to avoid
- // any mixup with instances.
- this.action_group = Gio.DBusActionGroup.get(
- this.g_connection,
- this.service.g_name_owner,
- this.g_object_path
- );
- this.menu = Gio.DBusMenuModel.get(
- this.g_connection,
- this.service.g_name_owner,
- this.g_object_path
- );
- // Poke the GMenu to ensure it's ready for us
- await new Promise((resolve, reject) => {
- this.g_connection.call(
- SERVICE_NAME,
- this.g_object_path,
- 'org.gtk.Menus',
- 'Start',
- new GLib.Variant('(au)', [[0]]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- (proxy, res) => {
- try {
- resolve(proxy.call_finish(res));
- } catch (e) {
- Gio.DBusError.strip_remote_error(e);
- reject(e);
- }
- }
- );
- });
- } catch (e) {
- this.destroy();
- throw e;
- }
- }
- destroy() {
- GObject.signal_handlers_destroy(this);
- }
- });
- /**
- * A simple proxy wrapper for the GSConnect service.
- */
- var Service = GObject.registerClass({
- GTypeName: 'GSConnectRemoteService',
- Implements: [Gio.DBusInterface],
- Properties: {
- 'active': GObject.ParamSpec.boolean(
- 'active',
- 'Active',
- 'Whether the service is active',
- GObject.ParamFlags.READABLE,
- false
- ),
- },
- Signals: {
- 'device-added': {
- flags: GObject.SignalFlags.RUN_FIRST,
- param_types: [Device.$gtype],
- },
- 'device-removed': {
- flags: GObject.SignalFlags.RUN_FIRST,
- param_types: [Device.$gtype],
- },
- },
- }, class Service extends Gio.DBusProxy {
- _init() {
- super._init({
- g_bus_type: Gio.BusType.SESSION,
- g_name: SERVICE_NAME,
- g_object_path: SERVICE_PATH,
- g_interface_name: 'org.freedesktop.DBus.ObjectManager',
- g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
- });
- this._active = false;
- this._devices = new Map();
- this._starting = false;
- // Watch the service
- this._nameOwnerChangedId = this.connect(
- 'notify::g-name-owner',
- this._onNameOwnerChanged.bind(this)
- );
- }
- get active() {
- return this._active;
- }
- get devices() {
- return Array.from(this._devices.values());
- }
- vfunc_g_signal(sender_name, signal_name, parameters) {
- try {
- // Don't emit signals until the ObjectManager has started
- if (!this.active)
- return;
- parameters = parameters.deepUnpack();
- switch (true) {
- case (signal_name === 'InterfacesAdded'):
- this._onInterfacesAdded(...parameters);
- break;
- case (signal_name === 'InterfacesRemoved'):
- this._onInterfacesRemoved(...parameters);
- break;
- }
- } catch (e) {
- logError(e);
- }
- }
- /**
- * org.freedesktop.DBus.ObjectManager.InterfacesAdded
- *
- * @param {string} object_path - Path interfaces have been added to
- * @param {Object} interfaces - A dictionary of interface objects
- */
- async _onInterfacesAdded(object_path, interfaces) {
- try {
- // An empty list means only the object has been added
- if (Object.values(interfaces).length === 0)
- return;
- // Skip existing proxies
- if (this._devices.has(object_path))
- return;
- // Create a proxy
- const device = new Device(this, object_path);
- await device.start();
- // Hold the proxy and emit ::device-added
- this._devices.set(object_path, device);
- this.emit('device-added', device);
- } catch (e) {
- logError(e, object_path);
- }
- }
- /**
- * org.freedesktop.DBus.ObjectManager.InterfacesRemoved
- *
- * @param {string} object_path - Path interfaces have been removed from
- * @param {string[]} interfaces - List of interface names removed
- */
- _onInterfacesRemoved(object_path, interfaces) {
- try {
- // An empty interface list means the object is being removed
- if (interfaces.length === 0)
- return;
- // Get the proxy
- const device = this._devices.get(object_path);
- if (device === undefined)
- return;
- // Release the proxy and emit ::device-removed
- this._devices.delete(object_path);
- this.emit('device-removed', device);
- // Destroy the device and force disposal
- device.destroy();
- } catch (e) {
- logError(e, object_path);
- }
- }
- async _addDevices() {
- const objects = await new Promise((resolve, reject) => {
- this.call(
- 'GetManagedObjects',
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- (proxy, res) => {
- try {
- const variant = proxy.call_finish(res);
- resolve(variant.deepUnpack()[0]);
- } catch (e) {
- Gio.DBusError.strip_remote_error(e);
- reject(e);
- }
- }
- );
- });
- for (const [object_path, object] of Object.entries(objects))
- await this._onInterfacesAdded(object_path, object);
- }
- _clearDevices() {
- for (const [object_path, device] of this._devices) {
- this._devices.delete(object_path);
- this.emit('device-removed', device);
- device.destroy();
- }
- }
- async _onNameOwnerChanged() {
- try {
- // If the service stopped, remove each device and mark it inactive
- if (this.g_name_owner === null) {
- this._clearDevices();
- this._active = false;
- this.notify('active');
- // If the service started, mark it active and add each device
- } else {
- this._active = true;
- this.notify('active');
- await this._addDevices();
- }
- } catch (e) {
- logError(e);
- }
- }
- /**
- * Reload all devices without affecting the remote service. This amounts to
- * removing and adding each device while emitting the appropriate signals.
- */
- async reload() {
- try {
- if (this._starting === false) {
- this._starting = true;
- this._clearDevices();
- await _proxyInit(this);
- await this._onNameOwnerChanged();
- this._starting = false;
- }
- } catch (e) {
- this._starting = false;
- throw e;
- }
- }
- /**
- * Start the service
- */
- async start() {
- try {
- if (this._starting === false && this.active === false) {
- this._starting = true;
- await _proxyInit(this);
- await this._onNameOwnerChanged();
- // Activate the service if it's not already running
- if (!this.active) {
- await new Promise((resolve, reject) => {
- this.g_connection.call(
- SERVICE_NAME,
- SERVICE_PATH,
- 'org.freedesktop.Application',
- 'Activate',
- GLib.Variant.new('(a{sv})', [{}]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- (proxy, res) => {
- try {
- resolve(proxy.call_finish(res));
- } catch (e) {
- Gio.DBusError.strip_remote_error(e);
- reject(e);
- }
- }
- );
- });
- }
- this._starting = false;
- }
- } catch (e) {
- this._starting = false;
- throw e;
- }
- }
- /**
- * Stop the service
- */
- stop() {
- if (this.active)
- this.activate_action('quit');
- }
- activate_action(name, parameter = null) {
- try {
- const paramArray = [];
- if (parameter instanceof GLib.Variant)
- paramArray[0] = parameter;
- const connection = this.g_connection || Gio.DBus.session;
- connection.call(
- SERVICE_NAME,
- SERVICE_PATH,
- 'org.freedesktop.Application',
- 'ActivateAction',
- GLib.Variant.new('(sava{sv})', [name, paramArray, {}]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null,
- null
- );
- } catch (e) {
- logError(e);
- }
- }
- destroy() {
- if (this._nameOwnerChangedId > 0) {
- this.disconnect(this._nameOwnerChangedId);
- this._nameOwnerChangedId = 0;
- this._clearDevices();
- this._active = false;
- GObject.signal_handlers_destroy(this);
- }
- }
- });
|