Files
calls/src/calls-ringer.c
Guido Günther b5af7f5e7d calls-ringer: Don't restart when feedback got ended explicitly
If a user e.g. lowers the gobal feedback level we don't want to
retrigger the events.

Without this calls assumes events ended naturally and restarts the
ringing without any events. As that is never ended even subsequent calls
don't receive any ringing.

Part-of: <https://gitlab.gnome.org/GNOME/calls/-/merge_requests/734>
2024-06-23 12:02:59 +00:00

529 lines
13 KiB
C

/*
* Copyright (C) 2018 Purism SPC
*
* This file is part of Calls.
*
* Calls 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, either version 3 of the License, or
* (at your option) any later version.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Bob Ham <bob.ham@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#define G_LOG_DOMAIN "CallsRinger"
#include "calls-config.h"
#include "calls-manager.h"
#include "calls-ringer.h"
#include "calls-ui-call-data.h"
#include "enum-types.h"
#include <glib/gi18n.h>
#include <glib-object.h>
#define LIBFEEDBACK_USE_UNSTABLE_API
#include <libfeedback.h>
enum {
PROP_0,
PROP_RING_STATE,
PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];
struct _CallsRinger {
GObject parent_instance;
GList *calls;
LfbEvent *ringing_event;
LfbEvent *ringing_soft_event;
GCancellable *cancel;
CallsRingState state;
CallsRingState target_state;
gboolean freeze_state_notify;
};
G_DEFINE_TYPE (CallsRinger, calls_ringer, G_TYPE_OBJECT)
static void target_state_step (CallsRinger *self);
static const char *
ring_state_to_string (CallsRingState state)
{
g_autoptr (GEnumClass) enum_class = NULL;
GEnumValue *enum_val;
enum_class = g_type_class_ref (CALLS_TYPE_RING_STATE);
enum_val = g_enum_get_value (enum_class, state);
return enum_val ? enum_val->value_nick : "invalid-state";
}
static void
set_ring_state (CallsRinger *self,
CallsRingState state)
{
g_assert (CALLS_IS_RINGER (self));
g_assert (state >= CALLS_RING_STATE_INACTIVE && state <= CALLS_RING_STATE_ERROR);
if (self->state == state)
return;
g_debug ("Setting ring state to '%s'", ring_state_to_string (state));
self->state = state;
if (state == self->target_state)
self->freeze_state_notify = FALSE;
else
target_state_step (self);
if (!self->freeze_state_notify)
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RING_STATE]);
}
static void
on_event_triggered (LfbEvent *event,
GAsyncResult *res,
CallsRinger *self)
{
g_autoptr (GError) err = NULL;
CallsRingState state;
gboolean ok;
g_return_if_fail (LFB_IS_EVENT (event));
ok = lfb_event_trigger_feedback_finish (event, res, &err);
g_debug ("Feedback '%s' triggered %ssuccessfully",
lfb_event_get_event (event),
ok ? "" : "un");
if (!CALLS_IS_RINGER (self)) {
g_warning ("Event feedback triggered, but ringer is gone");
return;
}
if (!ok) {
if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
g_debug ("Feedback has been cancelled");
else
g_warning ("Failed to trigger feedback for '%s': %s",
lfb_event_get_event (event), err->message);
set_ring_state (self, CALLS_RING_STATE_ERROR);
return;
}
if (event == self->ringing_event)
state = CALLS_RING_STATE_RINGING;
else
state = CALLS_RING_STATE_RINGING_SOFT;
set_ring_state (self, state);
}
static void
on_event_feedback_ended (LfbEvent *event,
GAsyncResult *res,
CallsRinger *self)
{
g_autoptr (GError) err = NULL;
gboolean ok;
g_assert (LFB_IS_EVENT (event));
ok = lfb_event_end_feedback_finish (event, res, &err);
g_debug ("Ended feedback '%s' %ssuccessfully",
lfb_event_get_event (event),
ok ? "" : "un");
if (!CALLS_IS_RINGER (self)) {
g_warning ("Event feedback ended, but ringer is gone");
return;
}
if (!ok) {
if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
g_debug ("Ending feedback has been cancelled");
else
g_warning ("Failed to end feedback for '%s': %s",
lfb_event_get_event (event), err->message);
set_ring_state (self, CALLS_RING_STATE_ERROR);
}
}
static void
on_feedback_ended (LfbEvent *event,
CallsRinger *self)
{
g_assert (LFB_IS_EVENT (event));
g_debug ("Feedback ended");
if (!CALLS_IS_RINGER (self)) {
g_warning ("Feedback stopped, but ringer is gone");
return;
}
/* When no feedback is available on the system (e.g. no vibration motor or LEDs)
* it will get ended immediately on triggering. Changing the target state will
* break loop that would otherwise occur. */
if (lfb_event_get_end_reason (event) == LFB_EVENT_END_REASON_NOT_FOUND ||
lfb_event_get_end_reason (event) == LFB_EVENT_END_REASON_EXPLICIT)
self->target_state = CALLS_RING_STATE_INACTIVE;
set_ring_state (self, CALLS_RING_STATE_INACTIVE);
}
static LfbEvent *
get_event_for_state (CallsRinger *self,
CallsRingState state)
{
switch (state) {
case CALLS_RING_STATE_RINGING:
return self->ringing_event;
case CALLS_RING_STATE_RINGING_SOFT:
return self->ringing_soft_event;
default:
g_assert_not_reached ();
}
}
static void
target_state_step (CallsRinger *self)
{
LfbEvent *event;
g_assert (CALLS_IS_RINGER (self));
if (self->state == self->target_state ||
self->state == CALLS_RING_STATE_ERROR)
return;
g_clear_object (&self->cancel);
self->cancel = g_cancellable_new ();
if (self->state == CALLS_RING_STATE_INACTIVE) {
event = get_event_for_state (self, self->target_state);
lfb_event_trigger_feedback_async (event,
self->cancel,
(GAsyncReadyCallback) on_event_triggered,
self);
} else {
gboolean needs_restart =
self->target_state == CALLS_RING_STATE_RINGING ||
self->target_state == CALLS_RING_STATE_RINGING_SOFT;
if (needs_restart)
self->freeze_state_notify = TRUE;
event = get_event_for_state (self, self->state);
lfb_event_end_feedback_async (event,
self->cancel,
(GAsyncReadyCallback) on_event_feedback_ended,
self);
}
}
static void
set_target_state (CallsRinger *self,
CallsRingState state)
{
g_assert (CALLS_IS_RINGER (self));
g_assert (state >= CALLS_RING_STATE_INACTIVE &&
state <= CALLS_RING_STATE_RINGING_SOFT);
if (self->target_state == state)
return;
self->target_state = state;
target_state_step (self);
}
static inline gboolean
is_ring_state (CuiCallState state)
{
return state == CUI_CALL_STATE_INCOMING;
}
static inline gboolean
is_active_state (CuiCallState state)
{
switch (state) {
case CUI_CALL_STATE_ACTIVE:
case CUI_CALL_STATE_CALLING:
case CUI_CALL_STATE_HELD:
return TRUE;
default:
return FALSE;
}
}
static gboolean
have_active_call (CallsRinger *self)
{
g_assert (CALLS_IS_RINGER (self));
for (GList *node = self->calls; node != NULL; node = node->next) {
CallsUiCallData *call = node->data;
if (is_active_state (cui_call_get_state (CUI_CALL (call))))
return TRUE;
}
return FALSE;
}
static gboolean
have_incoming_call (CallsRinger *self)
{
g_assert (CALLS_IS_RINGER (self));
for (GList *node = self->calls; node != NULL; node = node->next) {
CallsUiCallData *call = node->data;
if (is_ring_state (cui_call_get_state (CUI_CALL (call))) &&
!calls_ui_call_data_get_silenced (call) &&
calls_ui_call_data_get_ui_active (call))
return TRUE;
}
return FALSE;
}
static void
update_ring (CallsRinger *self)
{
CallsRingState target_state;
g_assert (CALLS_IS_RINGER (self));
if (self->state == CALLS_RING_STATE_ERROR) {
g_debug ("Can't ring because we're in an error state");
return;
}
if (have_incoming_call (self))
target_state = !have_active_call (self) ? CALLS_RING_STATE_RINGING : CALLS_RING_STATE_RINGING_SOFT;
else
target_state = CALLS_RING_STATE_INACTIVE;
set_target_state (self, target_state);
}
static void
call_added_cb (CallsRinger *self,
CallsUiCallData *call)
{
g_assert (CALLS_IS_RINGER (self));
g_assert (CALLS_IS_UI_CALL_DATA (call));
self->calls = g_list_append (self->calls, call);
g_object_connect (call,
"swapped-signal::notify::state", G_CALLBACK (update_ring), self,
"swapped-signal::notify::silenced", G_CALLBACK (update_ring), self,
"swapped-signal::notify::ui-active", G_CALLBACK (update_ring), self,
NULL);
update_ring (self);
}
static void
call_removed_cb (CallsRinger *self,
CallsUiCallData *call)
{
self->calls = g_list_remove (self->calls, call);
g_signal_handlers_disconnect_by_data (call, self);
update_ring (self);
}
static void
calls_ringer_init (CallsRinger *self)
{
g_autoptr (GError) err = NULL;
if (lfb_init (APP_ID, &err)) {
self->ringing_event = lfb_event_new ("phone-incoming-call");
/* Let feedbackd do the loop */
lfb_event_set_timeout (self->ringing_event, 0);
g_signal_connect (self->ringing_event,
"feedback-ended",
G_CALLBACK (on_feedback_ended),
self);
/* FIXME change event once it's available in feedbackd,
* see https://source.puri.sm/Librem5/feedbackd/-/issues/37
*/
self->ringing_soft_event = lfb_event_new ("phone-incoming-call");
lfb_event_set_timeout (self->ringing_soft_event, 15);
lfb_event_set_feedback_profile (self->ringing_soft_event, "quiet");
g_signal_connect (self->ringing_soft_event,
"feedback-ended",
G_CALLBACK (on_feedback_ended),
self);
} else {
g_warning ("Failed to init libfeedback: %s", err->message);
set_ring_state (self, CALLS_RING_STATE_ERROR);
}
}
static void
get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_RING_STATE:
g_value_set_enum (value, calls_ringer_get_state (CALLS_RINGER (object)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
constructed (GObject *object)
{
g_autoptr (GList) calls = NULL;
GList *c;
CallsRinger *self = CALLS_RINGER (object);
g_signal_connect_swapped (calls_manager_get_default (),
"ui-call-added",
G_CALLBACK (call_added_cb),
self);
g_signal_connect_swapped (calls_manager_get_default (),
"ui-call-removed",
G_CALLBACK (call_removed_cb),
self);
calls = calls_manager_get_calls (calls_manager_get_default ());
for (c = calls; c != NULL; c = c->next) {
call_added_cb (self, c->data);
}
G_OBJECT_CLASS (calls_ringer_parent_class)->constructed (object);
}
static void
dispose (GObject *object)
{
CallsRinger *self = CALLS_RINGER (object);
g_signal_handlers_disconnect_by_data (calls_manager_get_default (), self);
g_cancellable_cancel (self->cancel);
g_clear_object (&self->cancel);
G_OBJECT_CLASS (calls_ringer_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
CallsRinger *self = CALLS_RINGER (object);
if (lfb_is_initted ())
lfb_uninit ();
g_clear_object (&self->ringing_event);
g_clear_object (&self->ringing_soft_event);
G_OBJECT_CLASS (calls_ringer_parent_class)->finalize (object);
}
static void
calls_ringer_class_init (CallsRingerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = constructed;
object_class->dispose = dispose;
object_class->finalize = finalize;
object_class->get_property = get_property;
props[PROP_RING_STATE] =
g_param_spec_enum ("state",
"",
"",
CALLS_TYPE_RING_STATE,
CALLS_RING_STATE_INACTIVE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class,
PROP_LAST_PROP,
props);
}
CallsRinger *
calls_ringer_new (void)
{
return g_object_new (CALLS_TYPE_RINGER, NULL);
}
/**
* calls_ringer_get_state
* @self: A #CallsRinger
*
* Returns: The #CallsRingerState
*/
CallsRingState
calls_ringer_get_state (CallsRinger *self)
{
g_return_val_if_fail (CALLS_IS_RINGER (self), CALLS_RING_STATE_INACTIVE);
return self->state;
}