/* * Copyright (C) 2020 Purism SPC * SPDX-License-Identifier: GPL-3.0+ * Author: Guido Günther */ #include "libfeedback.h" #include "lfb-priv.h" #include /** * 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; }