Files
feedbackd/libfeedback/lfb-event.c
T
Guido Günther 49dab605c2 libfeedback: Cancel all running feedbacks on uninit
We keep track of the active ids and end them on uninit. This
make sure feedbacks don't continue to run if the application quits
and forgets to end a feedback.
2020-06-02 16:36:24 +00:00

663 lines
18 KiB
C

/*
* Copyright (C) 2020 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
* Author: Guido Günther <agx@sigxcpu.org>
*/
#include "libfeedback.h"
#include "lfb-priv.h"
#include <gio/gio.h>
/**
* SECTION:lfb-event
* @Short_description: An event triggering feedback to the user
* @Title: LfbEvent
*
* #LfbEvent represents an event that should trigger
* audio, haptic and/or visual feedback to the user by triggering
* feedback on a feedback daemon.
*
* One event can trigger multiple feedbacks at once (e.g. audio and
* haptic feedback). This is determined by the feedback theme in
* use (which is not under the appliction's control) and the active
* feedback profile (see #lfb_set_feedback_profile()).
*
* After initializing the library via #lfb_init() feedback can be
* triggered like:
*
* |[
* g_autoptr (GError) err = NULL;
* LpfEvent *event = lfb_event_new ("message-new-instant");
* lfb_event_set_timeout (event, 0);
* if (!lfb_event_trigger_feedback (event, &err))
* g_warning ("Failed to trigger feedback: %s", err->message);
* ]|
*
* When all feedback for this event has ended the #LfbEvent::feedback-ended
* signal is emitted. If you want to end the feedback ahead of time use
* #lfb_event_end_feedback ():
*
* |[
* if (!lfb_event_end_feedback (event, &err))
* g_warning ("Failed to end feedback: %s", err->message);
* ]|
*
* Since these methods involve DBus calls there are asynchronous variants
* available, e.g. #lfb_event_trigger_feedback_async():
*
* |[
* static void
* on_feedback_triggered (LfbEvent *event,
* GAsyncResult *res,
* gpointer unused)
* {
* g_autoptr (GError) err = NULL;
* if (!lfb_event_trigger_feedback_finish (event, res, &err)) {
* g_warning ("Failed to trigger feedback for %s: %s",
* lfb_event_get_event (event), err->message);
* }
* }
*
* static void
* my_function ()
* {
* LfbEvent *event = lfb_event_new ("message-new-instant");
* lfb_event_trigger_feedback_async (event, NULL,
* (GAsyncReadyCallback)on_feedback_triggered,
* NULL);
* }
* ]|
*/
enum {
PROP_0,
PROP_EVENT,
PROP_TIMEOUT,
PROP_STATE,
PROP_END_REASON,
PROP_LAST_PROP,
};
static GParamSpec *props[PROP_LAST_PROP];
enum {
SIGNAL_FEEDBACK_ENDED,
N_SIGNALS,
};
static guint signals[N_SIGNALS];
typedef struct _LfbEvent {
GObject parent;
char *event;
gint timeout;
guint id;
LfbEventState state;
gint end_reason;
gulong handler_id;
} LfbEvent;
G_DEFINE_TYPE (LfbEvent, lfb_event, G_TYPE_OBJECT);
typedef struct _LpfAsyncData {
LfbEvent *event;
GTask *task;
} LpfAsyncData;
static void
lfb_event_set_state (LfbEvent *self, LfbEventState state)
{
if (self->state == state)
return;
self->state = state;
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]);
}
static void
lfb_event_set_end_reason (LfbEvent *self, LfbEventEndReason reason)
{
if (self->end_reason == reason)
return;
self->end_reason = reason;
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_REASON]);
}
static GVariant *
build_hints (void)
{
GVariantBuilder hints_builder;
g_variant_builder_init (&hints_builder, G_VARIANT_TYPE ("a{sv}"));
return g_variant_new ("a{sv}", &hints_builder);
}
static void
on_trigger_feedback_finished (LfbGdbusFeedback *proxy,
GAsyncResult *res,
LpfAsyncData *data)
{
GTask *task = data->task;
LfbEvent *self = data->event;
GError *err = NULL;
gboolean success;
LfbEventState state;
g_return_if_fail (G_IS_TASK (task));
g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
g_return_if_fail (LFB_IS_EVENT (self));
success = lfb_gdbus_feedback_call_trigger_feedback_finish (proxy,
&self->id,
res,
&err);
if (!success) {
g_task_return_error (task, err);
state = LFB_EVENT_STATE_ERRORED;
} else {
g_task_return_boolean (task, TRUE);
state = LFB_EVENT_STATE_RUNNING;
_lfb_active_add_id (self->id);
}
lfb_event_set_state (self, state);
g_free (data);
g_object_unref (task);
g_object_unref (self);
}
static void
on_end_feedback_finished (LfbGdbusFeedback *proxy,
GAsyncResult *res,
LpfAsyncData *data)
{
GTask *task = data->task;
LfbEvent *self = data->event;
GError *err = NULL;
gboolean success;
g_return_if_fail (G_IS_TASK (task));
g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
g_return_if_fail (LFB_IS_EVENT (self));
success = lfb_gdbus_feedback_call_end_feedback_finish (proxy,
res,
&err);
if (!success) {
g_task_return_error (task, err);
} else
g_task_return_boolean (task, TRUE);
g_free (data);
g_object_unref (task);
g_object_unref (self);
}
static void
lfb_event_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
LfbEvent *self = LFB_EVENT (object);
switch (property_id) {
case PROP_EVENT:
g_free (self->event);
self->event = g_value_dup_string (value);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EVENT]);
break;
case PROP_TIMEOUT:
lfb_event_set_timeout (self, g_value_get_int (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
lfb_event_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
LfbEvent *self = LFB_EVENT (object);
switch (property_id) {
case PROP_EVENT:
g_value_set_string (value, self->event);
break;
case PROP_TIMEOUT:
g_value_set_int (value, self->timeout);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
lfb_event_finalize (GObject *object)
{
LfbEvent *self = LFB_EVENT (object);
/* Signal handler is disconnected automatically due to g_signal_connect_object */
self->handler_id = 0;
g_clear_pointer (&self->event, g_free);
G_OBJECT_CLASS (lfb_event_parent_class)->finalize (object);
}
static void
lfb_event_class_init (LfbEventClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->set_property = lfb_event_set_property;
object_class->get_property = lfb_event_get_property;
object_class->finalize = lfb_event_finalize;
/**
* LfbEvent:event:
*
* The type of event from the Event naming spec, e.g. 'message-new-instant'.
*/
props[PROP_EVENT] =
g_param_spec_string (
"event",
"Event",
"The name of the event triggering the feedback",
NULL,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
/**
* LfbEvent:timeout:
*
* How long feedback should be provided in milliseconds. The special value
* %-1 uses the natural length of each feedback while %0 plays each feedback
* in a loop until ended explicitly via e.g. #lfb_event_end_feedback().
*/
props[PROP_TIMEOUT] =
g_param_spec_int (
"timeout",
"Timeout",
"When the event should timeout",
-1, G_MAXINT, -1,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
props[PROP_STATE] =
g_param_spec_enum (
"state",
"State",
"The event's state",
LFB_TYPE_EVENT_STATE,
LFB_EVENT_END_REASON_NATURAL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_END_REASON] =
g_param_spec_enum (
"end-reason",
"End reason",
"The reason why the feedbacks ended",
LFB_TYPE_EVENT_END_REASON,
LFB_EVENT_END_REASON_NATURAL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
/**
* LfbEvent::feedback-ended:
*
* Emitted when all feedbacks triggered by the event have ended.
*/
signals[SIGNAL_FEEDBACK_ENDED] = g_signal_new ("feedback-ended",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST, 0, NULL, NULL,
NULL,
G_TYPE_NONE,
0);
}
static void
lfb_event_init (LfbEvent *self)
{
self->timeout = -1;
self->state = LFB_EVENT_STATE_NONE;
self->end_reason = LFB_EVENT_END_REASON_NATURAL;
}
/**
* lfb_event_new:
* @event: The event's name.
*
* Creates a new #LfbEvent based on the given event
* name. See #LfbEvent:event for details.
*
* Returns: The #LfbEvent.
*/
LfbEvent *
lfb_event_new (const char *event)
{
return g_object_new (LFB_TYPE_EVENT, "event", event, NULL);
}
static void
on_feedback_ended (LfbEvent *self,
guint event_id,
guint reason,
LfbGdbusFeedback *proxy)
{
g_return_if_fail (LFB_IS_EVENT (self));
g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
if (event_id != self->id)
return;
lfb_event_set_end_reason (self, reason);
lfb_event_set_state (self, LFB_EVENT_STATE_ENDED);
g_signal_emit (self, signals[SIGNAL_FEEDBACK_ENDED], 0);
_lfb_active_remove_id (self->id);
self->id = 0;
g_signal_handler_disconnect (proxy, self->handler_id);
self->handler_id = 0;
}
/**
* lfb_event_trigger_feedback:
* @self: The event to trigger feedback for.
* @error: The returned error information.
*
* Tells the feedback server to provide proper feedback for the give
* event to the user.
*
* Returns: %TRUE if successful. On error, this will return %FALSE and set
* @error.
*/
gboolean
lfb_event_trigger_feedback (LfbEvent *self, GError **error)
{
LfbGdbusFeedback *proxy;
gboolean success;
g_return_val_if_fail (LFB_IS_EVENT (self), FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
if (!lfb_is_initted ()) {
g_warning ("you must call lfb_init() before triggering events");
g_assert_not_reached ();
}
proxy = _lfb_get_proxy ();
g_return_val_if_fail (G_IS_DBUS_PROXY (proxy), FALSE);
if (self->handler_id == 0) {
self->handler_id = g_signal_connect_object (proxy,
"feedback-ended",
G_CALLBACK (on_feedback_ended),
self,
G_CONNECT_SWAPPED);
}
success = lfb_gdbus_feedback_call_trigger_feedback_sync (proxy,
lfb_get_app_id (),
self->event,
build_hints (),
self->timeout,
&self->id,
NULL,
error);
if (success)
_lfb_active_add_id (self->id);
lfb_event_set_state (self, success ? LFB_EVENT_STATE_RUNNING : LFB_EVENT_STATE_ERRORED);
return success;
}
/**
* lfb_event_trigger_feedback_async:
* @self: The event to trigger feedback for.
* @cancellable: (nullable): A #GCancellable or %NULL.
* @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
* @user_data: User data to pass to @callback.
*
* Tells the feedback server to provide proper feedback for the give
* event to the user. This is the sync version of
* #lfb_event_trigger_feedback.
*/
void
lfb_event_trigger_feedback_async (LfbEvent *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
LpfAsyncData *data;
LfbGdbusFeedback *proxy;
g_return_if_fail (LFB_IS_EVENT (self));
if (!lfb_is_initted ()) {
g_warning ("you must call lfb_init() before triggering events");
g_assert_not_reached ();
}
proxy = _lfb_get_proxy ();
g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
data = g_new0 (LpfAsyncData, 1);
data->task = g_task_new (self, cancellable, callback, user_data);
data->event = g_object_ref (self);
lfb_gdbus_feedback_call_trigger_feedback (proxy,
lfb_get_app_id (),
self->event,
build_hints (),
self->timeout,
cancellable,
(GAsyncReadyCallback)on_trigger_feedback_finished,
data);
}
/**
* lfb_event_trigger_feedback_finish:
* @self: the event
* @res: Result object passed to the callback of
* #lfb_event_trigger_feedback_async
* @error: Return location for error
*
* Finish an async operation started by lfb_event_trigger_feedback_async. You
* must call this function in the callback to free memory and receive any
* errors which occurred.
*
* Returns: %TRUE if playing finished successfully
*/
gboolean
lfb_event_trigger_feedback_finish (LfbEvent *self,
GAsyncResult *res,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
return g_task_propagate_boolean (G_TASK (res), error);
}
/**
* lfb_event_end_feedback:
* @self: The event to end feedback for.
* @error: The returned error information.
*
* Tells the feedback server to end all feedback for the given event as
* soon as possible.
*
* Returns: %TRUE if successful. On error, this will return %FALSE and set
* @error.
*/
gboolean
lfb_event_end_feedback (LfbEvent *self, GError **error)
{
LfbGdbusFeedback *proxy;
g_return_val_if_fail (LFB_IS_EVENT (self), FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
if (!lfb_is_initted ()) {
g_warning ("you must call lfb_init() before ending events");
g_assert_not_reached ();
}
proxy = _lfb_get_proxy ();
g_return_val_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy), FALSE);
return lfb_gdbus_feedback_call_end_feedback_sync (proxy, self->id, NULL, error);
}
/**
* lfb_event_end_feedback_finish:
* @self: the event
* @res: Result object passed to the callback of
* #lfb_event_end_feedback_async
* @error: Return location for error
*
* Finish an async operation started by lfb_event_end_feedback_async. You
* must call this function in the callback to free memory and receive any
* errors which occurred.
*
* Returns: %TRUE if playing finished successfully
*/
gboolean
lfb_event_end_feedback_finish (LfbEvent *self,
GAsyncResult *res,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
return g_task_propagate_boolean (G_TASK (res), error);
}
/**
* lfb_event_end_feedback_async:
* @self: The event to end feedback for.
* @cancellable: (nullable): A #GCancellable or %NULL.
* @callback: A #GAsyncReadyCallback to call when the request is satisfied or %NULL.
* @user_data: User data to pass to @callback.
*
* Tells the feedback server to end all feedback for the given event as
* soon as possible.
*/
void
lfb_event_end_feedback_async (LfbEvent *self,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
LpfAsyncData *data;
LfbGdbusFeedback *proxy;
g_return_if_fail (LFB_IS_EVENT (self));
if (!lfb_is_initted ()) {
g_warning ("you must call lfb_init() before ending events");
g_assert_not_reached ();
}
proxy = _lfb_get_proxy ();
g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy));
data = g_new0 (LpfAsyncData, 1);
data->task = g_task_new (self, cancellable, callback, user_data);
data->event = g_object_ref (self);
lfb_gdbus_feedback_call_end_feedback (proxy,
self->id,
cancellable,
(GAsyncReadyCallback)on_end_feedback_finished,
data);
}
/**
* lfb_event_set_timeout:
* @self: The event
* @timeout: The timeout
*
* Tells the feedback server to end feedack after #timeout seconds.
* The value -1 indicates to not set a timeout and let feedbacks stop
* on their own while 0 indicates to loop all feedbacks endlessly.
* They must be stopped via #lfb_event_end_feedback () in this case.
*
* It is an error to change the timeout after the feedback has been triggered
* via lfb_event_trigger.
*/
void
lfb_event_set_timeout (LfbEvent *self, gint timeout)
{
g_return_if_fail (LFB_IS_EVENT (self));
if (self->timeout == timeout)
return;
self->timeout = timeout;
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIMEOUT]);
}
/**
* lfb_event_get_event:
* @self: The event
*
* Get the event's name according to the event naming spec.
*
* Returns: The event name
*/
const char *
lfb_event_get_event (LfbEvent *self)
{
g_return_val_if_fail (LFB_IS_EVENT (self), NULL);
return self->event;
}
/**
* lfb_event_get_timeout:
* @self: The event
*
* Get the currently set timeout.
*
* Returns: The event timeout in msecs
*/
gint
lfb_event_get_timeout (LfbEvent *self)
{
g_return_val_if_fail (LFB_IS_EVENT (self), -1);
return self->timeout;
}
/**
* lfb_event_get_state:
* @self: The event
*
* Get the current event state (e.g. if triggered feeedback is
* currently running.
*
* Returns: The state of the feedback triggered by event.
*/
LfbEventState
lfb_event_get_state (LfbEvent *self)
{
g_return_val_if_fail (LFB_IS_EVENT (self), LFB_EVENT_STATE_NONE);
return self->state;
}
/**
* lfb_event_get_end_reason:
* @self: The event
*
* Get the reason why the feadback ended.
*
* Returns: The reason why feedback ended.
*/
LfbEventEndReason
lfb_event_get_end_reason (LfbEvent *self)
{
g_return_val_if_fail (LFB_IS_EVENT (self), LFB_EVENT_END_REASON_NATURAL);
return self->end_reason;
}