plugin.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GLib from 'gi://GLib';
  6. import GObject from 'gi://GObject';
  7. import Config from '../config.js';
  8. import * as Core from './core.js';
  9. import plugins from './plugins/index.js';
  10. /**
  11. * Base class for device plugins.
  12. */
  13. const Plugin = GObject.registerClass({
  14. GTypeName: 'GSConnectPlugin',
  15. Properties: {
  16. 'device': GObject.ParamSpec.object(
  17. 'device',
  18. 'Device',
  19. 'The device that owns this plugin',
  20. GObject.ParamFlags.READABLE,
  21. GObject.Object
  22. ),
  23. 'name': GObject.ParamSpec.string(
  24. 'name',
  25. 'Name',
  26. 'The device name',
  27. GObject.ParamFlags.READABLE,
  28. null
  29. ),
  30. },
  31. }, class Plugin extends GObject.Object {
  32. _init(device, name, meta = null) {
  33. super._init();
  34. this._device = device;
  35. this._name = name;
  36. this._meta = meta;
  37. if (this._meta === null)
  38. this._meta = plugins[name].Metadata;
  39. // GSettings
  40. const schema = Config.GSCHEMA.lookup(this._meta.id, false);
  41. if (schema !== null) {
  42. this.settings = new Gio.Settings({
  43. settings_schema: schema,
  44. path: `${device.settings.path}plugin/${name}/`,
  45. });
  46. }
  47. // GActions
  48. this._gactions = [];
  49. if (this._meta.actions) {
  50. const menu = this.device.settings.get_strv('menu-actions');
  51. for (const name in this._meta.actions) {
  52. const info = this._meta.actions[name];
  53. this._registerAction(name, menu.indexOf(name), info);
  54. }
  55. }
  56. }
  57. get cancellable() {
  58. if (this._cancellable === undefined)
  59. this._cancellable = new Gio.Cancellable();
  60. return this._cancellable;
  61. }
  62. get device() {
  63. return this._device;
  64. }
  65. get name() {
  66. return this._name;
  67. }
  68. _activateAction(action, parameter) {
  69. try {
  70. let args = null;
  71. if (parameter instanceof GLib.Variant)
  72. args = parameter.full_unpack();
  73. if (Array.isArray(args))
  74. this[action.name](...args);
  75. else
  76. this[action.name](args);
  77. } catch (e) {
  78. logError(e, action.name);
  79. }
  80. }
  81. _registerAction(name, menuIndex, info) {
  82. try {
  83. // Device Action
  84. const action = new Gio.SimpleAction({
  85. name: name,
  86. parameter_type: info.parameter_type,
  87. enabled: false,
  88. });
  89. action.connect('activate', this._activateAction.bind(this));
  90. this.device.add_action(action);
  91. // Menu
  92. if (menuIndex > -1) {
  93. this.device.addMenuAction(
  94. action,
  95. menuIndex,
  96. info.label,
  97. info.icon_name
  98. );
  99. }
  100. this._gactions.push(action);
  101. } catch (e) {
  102. logError(e, `${this.device.name}: ${this.name}`);
  103. }
  104. }
  105. /**
  106. * Called when the device connects.
  107. */
  108. connected() {
  109. // Enabled based on device capabilities, which might change
  110. const incoming = this.device.settings.get_strv('incoming-capabilities');
  111. const outgoing = this.device.settings.get_strv('outgoing-capabilities');
  112. for (const action of this._gactions) {
  113. const info = this._meta.actions[action.name];
  114. if (info.incoming.every(type => outgoing.includes(type)) &&
  115. info.outgoing.every(type => incoming.includes(type)))
  116. action.set_enabled(true);
  117. }
  118. }
  119. /**
  120. * Called when the device disconnects.
  121. */
  122. disconnected() {
  123. for (const action of this._gactions)
  124. action.set_enabled(false);
  125. }
  126. /**
  127. * Called when a packet is received that the plugin is a handler for.
  128. *
  129. * @param {Core.Packet} packet - A KDE Connect packet
  130. */
  131. handlePacket(packet) {
  132. throw new GObject.NotImplementedError();
  133. }
  134. /**
  135. * Cache JSON parseable properties on this object for persistence. The
  136. * filename ~/.cache/gsconnect/<device-id>/<plugin-name>.json will be used
  137. * to store the properties and values.
  138. *
  139. * Calling cacheProperties() opens a JSON cache file and reads any stored
  140. * properties and values onto the current instance. When destroy()
  141. * is called the properties are automatically stored in the same file.
  142. *
  143. * @param {Array} names - A list of this object's property names to cache
  144. */
  145. async cacheProperties(names) {
  146. try {
  147. this._cacheProperties = names;
  148. // Ensure the device's cache directory exists
  149. const cachedir = GLib.build_filenamev([
  150. Config.CACHEDIR,
  151. this.device.id,
  152. ]);
  153. GLib.mkdir_with_parents(cachedir, 448);
  154. this._cacheFile = Gio.File.new_for_path(
  155. GLib.build_filenamev([cachedir, `${this.name}.json`]));
  156. // Read the cache from disk
  157. const [contents] = await this._cacheFile.load_contents_async(
  158. this.cancellable);
  159. const cache = JSON.parse(new TextDecoder().decode(contents));
  160. Object.assign(this, cache);
  161. } catch (e) {
  162. debug(e.message, `${this.device.name}: ${this.name}`);
  163. } finally {
  164. this.cacheLoaded();
  165. }
  166. }
  167. /**
  168. * An overridable function that is invoked when the on-disk cache is being
  169. * cleared. Implementations should use this function to clear any in-memory
  170. * cache data.
  171. */
  172. clearCache() {}
  173. /**
  174. * An overridable function that is invoked when the cache is done loading
  175. */
  176. cacheLoaded() {}
  177. /**
  178. * Unregister plugin actions, write the cache (if applicable) and destroy
  179. * any dangling signal handlers.
  180. */
  181. destroy() {
  182. // Cancel any pending plugin operations
  183. if (this._cancellable !== undefined)
  184. this._cancellable.cancel();
  185. for (const action of this._gactions) {
  186. this.device.removeMenuAction(`device.${action.name}`);
  187. this.device.remove_action(action.name);
  188. }
  189. // Write the cache to disk synchronously
  190. if (this._cacheFile !== undefined) {
  191. try {
  192. // Build the cache
  193. const cache = {};
  194. for (const name of this._cacheProperties)
  195. cache[name] = this[name];
  196. this._cacheFile.replace_contents(
  197. JSON.stringify(cache, null, 2),
  198. null,
  199. false,
  200. Gio.FileCreateFlags.REPLACE_DESTINATION,
  201. null
  202. );
  203. } catch (e) {
  204. debug(e.message, `${this.device.name}: ${this.name}`);
  205. }
  206. }
  207. GObject.signal_handlers_destroy(this);
  208. }
  209. });
  210. export default Plugin;