/*
* Copyright 2015 Canonical Ltd.
* Copyright 2021 Robert Tari
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Authors:
* Charles Kerr
* Robert Tari
*/
using PulseAudio;
public abstract class VolumeWarning : Object
{
// true if headphones are in use
public bool headphones_active { get; set; default = false; }
// true if the warning dialog is being shown
public bool active { get; protected set; default = false; }
// true if we're playing unapproved loud multimedia over headphones
public bool high_volume { get; protected set; default = false; }
public enum Key {
VOLUME_UP,
VOLUME_DOWN
}
public void user_keypress (Key key) {
if ((key == Key.VOLUME_DOWN) && active) {
_notification.close ();
on_user_response (IndicatorSound.WarnNotification.Response.CANCEL);
}
}
internal VolumeWarning (IndicatorSound.Options options) {
_options = options;
init_high_volume ();
init_approved ();
_notification.user_responded.connect ((n, r) => on_user_response (r));
}
~VolumeWarning () {
clear_timer (ref _approved_timer);
}
/***
****
***/
// true if the user has approved high volumes recently
protected bool approved { get; set; default = false; }
// true if multimedia is currently playing
protected bool multimedia_active { get; set; default = false; }
/* Cached value of the multimedia volume reported by pulse.
Setting this only updates the cache -- to change the volume,
use sound_system_set_multimedia_volume.
NB: This PulseAudio.Volume is typed as uint to unconfuse valac. */
protected uint multimedia_volume { get; set; default = PulseAudio.Volume.INVALID; }
protected abstract void sound_system_set_multimedia_volume (PulseAudio.Volume volume);
protected void clear_timer (ref uint timer) {
if (timer != 0) {
Source.remove (timer);
timer = 0;
}
}
private IndicatorSound.Options _options;
/**
*** HIGH VOLUME PROPERTY
**/
private void init_high_volume () {
const string self_keys[] = {
"multimedia-volume",
"multimedia-active",
"headphones-active",
"high-volume-approved"
};
foreach (var key in self_keys)
this.notify[key].connect (() => update_high_volume ());
const string options_keys[] = {
"loud-volume",
"loud-warning-enabled"
};
foreach (var key in options_keys)
_options.notify[key].connect (() => update_high_volume ());
update_high_volume ();
}
private void update_high_volume () {
var newval = _options.loud_warning_enabled
&& headphones_active
&& multimedia_active
&& !approved
&& (multimedia_volume != PulseAudio.Volume.INVALID)
// from the sound specs:
// "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB
&& (multimedia_volume > _options.loud_volume);
if (high_volume != newval) {
debug ("changing high_volume from %d to %d", (int)high_volume, (int)newval);
if (newval && !active)
activate ();
high_volume = newval;
}
}
/**
*** HIGH VOLUME APPROVED PROPERTY
**/
private Settings _settings = new Settings ("org.ayatana.indicator.sound");
private const string TTL_KEY = "warning-volume-confirmation-ttl";
private uint _approved_timer = 0;
private int64 _approved_at = 0;
private int64 _approved_ttl_usec = 0;
private void approve_high_volume () {
_approved_at = GLib.get_monotonic_time ();
update_approved ();
update_approved_timer ();
}
private void init_approved () {
_settings.changed[TTL_KEY].connect (() => update_approved_cache ());
update_approved_cache ();
}
private void update_approved_cache () {
_approved_ttl_usec = _settings.get_int (TTL_KEY);
_approved_ttl_usec *= 1000000;
update_approved ();
update_approved_timer ();
}
private void update_approved_timer () {
clear_timer (ref _approved_timer);
if (_approved_at == 0)
return;
int64 expires_at = _approved_at + _approved_ttl_usec;
int64 now = GLib.get_monotonic_time ();
if (expires_at > now) {
var seconds_left = 1 + ((expires_at - now) / 1000000);
_approved_timer = Timeout.add_seconds ((uint)seconds_left, () => {
_approved_timer = 0;
update_approved ();
return Source.REMOVE;
});
}
}
private void update_approved () {
var new_approved = calculate_approved ();
if (approved != new_approved) {
debug ("changing approved from %d to %d", (int)approved, (int)new_approved);
approved = new_approved;
}
}
private bool calculate_approved () {
int64 now = GLib.get_monotonic_time ();
return (_approved_at != 0)
&& (_approved_at + _approved_ttl_usec >= now);
}
// NOTIFICATION
private IndicatorSound.WarnNotification _notification = new IndicatorSound.WarnNotification ();
private PulseAudio.Volume _ok_volume = PulseAudio.Volume.INVALID;
protected virtual void preshow () {}
private void activate () {
preshow ();
_ok_volume = multimedia_volume;
_notification.show ();
this.active = true;
// lower the volume to just the warning level
// from the sound specs:
// "Whenever you increase volume,..., such that acoustic output would be MORE than 85 dB
sound_system_set_multimedia_volume (_options.loud_volume);
}
private void on_user_response (IndicatorSound.WarnNotification.Response response) {
this.active = false;
if (response == IndicatorSound.WarnNotification.Response.OK) {
approve_high_volume ();
sound_system_set_multimedia_volume (_ok_volume);
} else {
this.cancel_pressed (VolumeWarning.volume_to_double(_options.loud_volume));
}
_ok_volume = PulseAudio.Volume.INVALID;
}
private static double volume_to_double (PulseAudio.Volume vol)
{
double tmp = (double)(vol - PulseAudio.Volume.MUTED);
return tmp / (double)(PulseAudio.Volume.NORM - PulseAudio.Volume.MUTED);
}
public signal void cancel_pressed (double cancel_volume);
}