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