telephony.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const GdkPixbuf = imports.gi.GdkPixbuf;
  6. const Gio = imports.gi.Gio;
  7. const GLib = imports.gi.GLib;
  8. const GObject = imports.gi.GObject;
  9. const Components = imports.service.components;
  10. const PluginBase = imports.service.plugin;
  11. var Metadata = {
  12. label: _('Telephony'),
  13. description: _('Be notified about calls and adjust system volume during ringing/ongoing calls'),
  14. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Telephony',
  15. incomingCapabilities: [
  16. 'kdeconnect.telephony',
  17. ],
  18. outgoingCapabilities: [
  19. 'kdeconnect.telephony.request',
  20. 'kdeconnect.telephony.request_mute',
  21. ],
  22. actions: {
  23. muteCall: {
  24. // TRANSLATORS: Silence the actively ringing call
  25. label: _('Mute Call'),
  26. icon_name: 'audio-volume-muted-symbolic',
  27. parameter_type: null,
  28. incoming: ['kdeconnect.telephony'],
  29. outgoing: ['kdeconnect.telephony.request_mute'],
  30. },
  31. },
  32. };
  33. /**
  34. * Telephony Plugin
  35. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/telephony
  36. * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/TelephonyPlugin
  37. */
  38. var Plugin = GObject.registerClass({
  39. GTypeName: 'GSConnectTelephonyPlugin',
  40. }, class Plugin extends PluginBase.Plugin {
  41. _init(device) {
  42. super._init(device, 'telephony');
  43. // Neither of these are crucial for the plugin to work
  44. this._mpris = Components.acquire('mpris');
  45. this._mixer = Components.acquire('pulseaudio');
  46. }
  47. handlePacket(packet) {
  48. switch (packet.type) {
  49. case 'kdeconnect.telephony':
  50. this._handleEvent(packet);
  51. break;
  52. }
  53. }
  54. /**
  55. * Change volume, microphone and media player state in response to an
  56. * incoming or answered call.
  57. *
  58. * @param {string} eventType - 'ringing' or 'talking'
  59. */
  60. _setMediaState(eventType) {
  61. // Mixer Volume
  62. if (this._mixer !== undefined) {
  63. switch (this.settings.get_string(`${eventType}-volume`)) {
  64. case 'restore':
  65. this._mixer.restore();
  66. break;
  67. case 'lower':
  68. this._mixer.lowerVolume();
  69. break;
  70. case 'mute':
  71. this._mixer.muteVolume();
  72. break;
  73. }
  74. if (eventType === 'talking' && this.settings.get_boolean('talking-microphone'))
  75. this._mixer.muteMicrophone();
  76. }
  77. // Media Playback
  78. if (this._mpris && this.settings.get_boolean(`${eventType}-pause`))
  79. this._mpris.pauseAll();
  80. }
  81. /**
  82. * Restore volume, microphone and media player state (if changed), making
  83. * sure to unpause before raising volume.
  84. *
  85. * TODO: there's a possibility we might revert a media/mixer state set for
  86. * another device.
  87. */
  88. _restoreMediaState() {
  89. // Media Playback
  90. if (this._mpris)
  91. this._mpris.unpauseAll();
  92. // Mixer Volume
  93. if (this._mixer)
  94. this._mixer.restore();
  95. }
  96. /**
  97. * Load a Gdk.Pixbuf from base64 encoded data
  98. *
  99. * @param {string} data - Base64 encoded JPEG data
  100. * @return {Gdk.Pixbuf|null} A contact photo
  101. */
  102. _getThumbnailPixbuf(data) {
  103. const loader = new GdkPixbuf.PixbufLoader();
  104. try {
  105. data = GLib.base64_decode(data);
  106. loader.write(data);
  107. loader.close();
  108. } catch (e) {
  109. debug(e, this.device.name);
  110. }
  111. return loader.get_pixbuf();
  112. }
  113. /**
  114. * Handle a telephony event (ringing, talking), showing or hiding a
  115. * notification and possibly adjusting the media/mixer state.
  116. *
  117. * @param {Core.Packet} packet - A `kdeconnect.telephony`
  118. */
  119. _handleEvent(packet) {
  120. // Only handle 'ringing' or 'talking' events; leave the notification
  121. // plugin to handle 'missedCall' since they're often repliable
  122. if (!['ringing', 'talking'].includes(packet.body.event))
  123. return;
  124. // This is the end of a telephony event
  125. if (packet.body.isCancel)
  126. this._cancelEvent(packet);
  127. else
  128. this._notifyEvent(packet);
  129. }
  130. _cancelEvent(packet) {
  131. // Ensure we have a sender
  132. // TRANSLATORS: No name or phone number
  133. let sender = _('Unknown Contact');
  134. if (packet.body.contactName)
  135. sender = packet.body.contactName;
  136. else if (packet.body.phoneNumber)
  137. sender = packet.body.phoneNumber;
  138. this.device.hideNotification(`${packet.body.event}|${sender}`);
  139. this._restoreMediaState();
  140. }
  141. _notifyEvent(packet) {
  142. let body;
  143. let buttons = [];
  144. let icon = null;
  145. let priority = Gio.NotificationPriority.NORMAL;
  146. // Ensure we have a sender
  147. // TRANSLATORS: No name or phone number
  148. let sender = _('Unknown Contact');
  149. if (packet.body.contactName)
  150. sender = packet.body.contactName;
  151. else if (packet.body.phoneNumber)
  152. sender = packet.body.phoneNumber;
  153. // If there's a photo, use it as the notification icon
  154. if (packet.body.phoneThumbnail)
  155. icon = this._getThumbnailPixbuf(packet.body.phoneThumbnail);
  156. if (icon === null)
  157. icon = new Gio.ThemedIcon({name: 'call-start-symbolic'});
  158. // Notify based based on the event type
  159. if (packet.body.event === 'ringing') {
  160. this._setMediaState('ringing');
  161. // TRANSLATORS: The phone is ringing
  162. body = _('Incoming call');
  163. buttons = [{
  164. action: 'muteCall',
  165. // TRANSLATORS: Silence the actively ringing call
  166. label: _('Mute'),
  167. parameter: null,
  168. }];
  169. priority = Gio.NotificationPriority.URGENT;
  170. }
  171. if (packet.body.event === 'talking') {
  172. this.device.hideNotification(`ringing|${sender}`);
  173. this._setMediaState('talking');
  174. // TRANSLATORS: A phone call is active
  175. body = _('Ongoing call');
  176. }
  177. this.device.showNotification({
  178. id: `${packet.body.event}|${sender}`,
  179. title: sender,
  180. body: body,
  181. icon: icon,
  182. priority: priority,
  183. buttons: buttons,
  184. });
  185. }
  186. /**
  187. * Silence an incoming call and restore the previous mixer/media state, if
  188. * applicable.
  189. */
  190. muteCall() {
  191. this.device.sendPacket({
  192. type: 'kdeconnect.telephony.request_mute',
  193. body: {},
  194. });
  195. this._restoreMediaState();
  196. }
  197. destroy() {
  198. if (this._mixer !== undefined)
  199. this._mixer = Components.release('pulseaudio');
  200. if (this._mpris !== undefined)
  201. this._mpris = Components.release('mpris');
  202. super.destroy();
  203. }
  204. });