1
0
Fork 0
mirror of https://gitlab.gnome.org/GNOME/calls.git synced 2024-11-16 05:15:36 +00:00
Purism-Calls/src/calls-ringer.c
Evangelos Ribeiro Tzaras f96c9ded63 ringer: Break loop if triggered event was ended immediately
When the only feedback of an event is unavailable on a system (e.g. no
vibration motor or LED) the "feedback-ended" signal is emitted
immediately and the end reason will be LFB_EVENT_END_REASON_NOT_FOUND.

In this case we need to change the target state, so that our logic does
not end up retriggering the event infinitely.
2022-12-03 20:55:37 +01:00

555 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 "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>
/* cancel lfb operations after reaching the timeout */
#define LFB_TIMEOUT_MS 1000
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;
guint request_timeout_id;
};
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);
g_clear_handle_id (&self->request_timeout_id, g_source_remove);
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' %ssucessfully",
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)
self->target_state = CALLS_RING_STATE_INACTIVE;
set_ring_state (self, CALLS_RING_STATE_INACTIVE);
}
static gboolean
request_timed_out (CallsRinger *self)
{
if (!CALLS_IS_RINGER (self))
return G_SOURCE_REMOVE;
g_debug ("Request '%s' timed out, cancelling..",
ring_state_to_string (self->target_state));
g_cancellable_cancel (self->cancel);
set_ring_state (self, CALLS_RING_STATE_ERROR);
return G_SOURCE_REMOVE;
}
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);
}
self->request_timeout_id =
g_timeout_add (LFB_TIMEOUT_MS, G_SOURCE_FUNC (request_timed_out), 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_clear_handle_id (&self->request_timeout_id, g_source_remove);
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;
}