246 lines
7.0 KiB
JavaScript
Raw Normal View History

2024-07-08 22:46:35 +02:00
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import GdkPixbuf from 'gi://GdkPixbuf';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import Plugin from '../plugin.js';
export const Metadata = {
label: _('Telephony'),
description: _('Be notified about calls and adjust system volume during ringing/ongoing calls'),
id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Telephony',
incomingCapabilities: [
'kdeconnect.telephony',
],
outgoingCapabilities: [
'kdeconnect.telephony.request',
'kdeconnect.telephony.request_mute',
],
actions: {
muteCall: {
// TRANSLATORS: Silence the actively ringing call
label: _('Mute Call'),
icon_name: 'audio-volume-muted-symbolic',
parameter_type: null,
incoming: ['kdeconnect.telephony'],
outgoing: ['kdeconnect.telephony.request_mute'],
},
},
};
/**
* Telephony Plugin
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/telephony
* https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/TelephonyPlugin
*/
const TelephonyPlugin = GObject.registerClass({
GTypeName: 'GSConnectTelephonyPlugin',
}, class TelephonyPlugin extends Plugin {
_init(device) {
super._init(device, 'telephony');
// Neither of these are crucial for the plugin to work
this._mpris = Components.acquire('mpris');
this._mixer = Components.acquire('pulseaudio');
}
handlePacket(packet) {
switch (packet.type) {
case 'kdeconnect.telephony':
this._handleEvent(packet);
break;
}
}
/**
* Change volume, microphone and media player state in response to an
* incoming or answered call.
*
* @param {string} eventType - 'ringing' or 'talking'
*/
_setMediaState(eventType) {
// Mixer Volume
if (this._mixer !== undefined) {
switch (this.settings.get_string(`${eventType}-volume`)) {
case 'restore':
this._mixer.restore();
break;
case 'lower':
this._mixer.lowerVolume();
break;
case 'mute':
this._mixer.muteVolume();
break;
}
if (eventType === 'talking' && this.settings.get_boolean('talking-microphone'))
this._mixer.muteMicrophone();
}
// Media Playback
if (this._mpris && this.settings.get_boolean(`${eventType}-pause`))
this._mpris.pauseAll();
}
/**
* Restore volume, microphone and media player state (if changed), making
* sure to unpause before raising volume.
*
* TODO: there's a possibility we might revert a media/mixer state set for
* another device.
*/
_restoreMediaState() {
// Media Playback
if (this._mpris)
this._mpris.unpauseAll();
// Mixer Volume
if (this._mixer)
this._mixer.restore();
}
/**
* Load a Gdk.Pixbuf from base64 encoded data
*
* @param {string} data - Base64 encoded JPEG data
* @return {Gdk.Pixbuf|null} A contact photo
*/
_getThumbnailPixbuf(data) {
const loader = new GdkPixbuf.PixbufLoader();
try {
data = GLib.base64_decode(data);
loader.write(data);
loader.close();
} catch (e) {
debug(e, this.device.name);
}
return loader.get_pixbuf();
}
/**
* Handle a telephony event (ringing, talking), showing or hiding a
* notification and possibly adjusting the media/mixer state.
*
* @param {Core.Packet} packet - A `kdeconnect.telephony`
*/
_handleEvent(packet) {
// Only handle 'ringing' or 'talking' events; leave the notification
// plugin to handle 'missedCall' since they're often repliable
if (!['ringing', 'talking'].includes(packet.body.event))
return;
// This is the end of a telephony event
if (packet.body.isCancel)
this._cancelEvent(packet);
else
this._notifyEvent(packet);
}
_cancelEvent(packet) {
// Ensure we have a sender
// TRANSLATORS: No name or phone number
let sender = _('Unknown Contact');
if (packet.body.contactName)
sender = packet.body.contactName;
else if (packet.body.phoneNumber)
sender = packet.body.phoneNumber;
this.device.hideNotification(`${packet.body.event}|${sender}`);
this._restoreMediaState();
}
_notifyEvent(packet) {
let body;
let buttons = [];
let icon = null;
let priority = Gio.NotificationPriority.NORMAL;
// Ensure we have a sender
// TRANSLATORS: No name or phone number
let sender = _('Unknown Contact');
if (packet.body.contactName)
sender = packet.body.contactName;
else if (packet.body.phoneNumber)
sender = packet.body.phoneNumber;
// If there's a photo, use it as the notification icon
if (packet.body.phoneThumbnail)
icon = this._getThumbnailPixbuf(packet.body.phoneThumbnail);
if (icon === null)
icon = new Gio.ThemedIcon({name: 'call-start-symbolic'});
// Notify based based on the event type
if (packet.body.event === 'ringing') {
this._setMediaState('ringing');
// TRANSLATORS: The phone is ringing
body = _('Incoming call');
buttons = [{
action: 'muteCall',
// TRANSLATORS: Silence the actively ringing call
label: _('Mute'),
parameter: null,
}];
priority = Gio.NotificationPriority.URGENT;
}
if (packet.body.event === 'talking') {
this.device.hideNotification(`ringing|${sender}`);
this._setMediaState('talking');
// TRANSLATORS: A phone call is active
body = _('Ongoing call');
}
this.device.showNotification({
id: `${packet.body.event}|${sender}`,
title: sender,
body: body,
icon: icon,
priority: priority,
buttons: buttons,
});
}
/**
* Silence an incoming call and restore the previous mixer/media state, if
* applicable.
*/
muteCall() {
this.device.sendPacket({
type: 'kdeconnect.telephony.request_mute',
body: {},
});
this._restoreMediaState();
}
destroy() {
if (this._mixer !== undefined)
this._mixer = Components.release('pulseaudio');
if (this._mpris !== undefined)
this._mpris = Components.release('mpris');
super.destroy();
}
});
export default TelephonyPlugin;