plugin.js 7.0 KB

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