If a custom sound is provided for the event, use that instead of the one from the theme. We always add the sound event, even when the theme doesn't have sound feedback for that event honoring the users choice. Closes: https://source.puri.sm/Librem5/feedbackd/-/merge_requests/150 Signed-off-by: Guido Günther <agx@sigxcpu.org>
768 lines
23 KiB
C
768 lines
23 KiB
C
/*
|
|
* Copyright (C) 2020 Purism SPC
|
|
* 2024-2025 The Phosh Developers
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0+
|
|
* Author: Guido Günther <agx@sigxcpu.org>
|
|
*/
|
|
|
|
#define G_LOG_DOMAIN "fbd-feedback-manager"
|
|
|
|
#include "lfb-names.h"
|
|
#include "fbd.h"
|
|
#include "fbd-dev-vibra.h"
|
|
#include "fbd-dev-leds.h"
|
|
#include "fbd-event.h"
|
|
#include "fbd-feedback-vibra.h"
|
|
#include "fbd-feedback-vibra-pattern.h"
|
|
#include "fbd-feedback-manager.h"
|
|
#include "fbd-feedback-theme.h"
|
|
#include "fbd-haptic-manager.h"
|
|
#include "fbd-theme-expander.h"
|
|
|
|
#include <gmobile.h>
|
|
|
|
#include <gio/gio.h>
|
|
#include <glib-unix.h>
|
|
#include <gudev/gudev.h>
|
|
|
|
#define FEEDBACKD_KEY_PROFILE "profile"
|
|
#define FEEDBACKD_KEY_THEME "theme"
|
|
#define FEEDBACKD_KEY_ALLOW_IMPORTANT "allow-important"
|
|
|
|
#define APP_SCHEMA FEEDBACKD_SCHEMA_ID ".application"
|
|
#define APP_PREFIX "/org/sigxcpu/feedbackd/application/"
|
|
|
|
#define FEEDBACKD_THEME_VAR "FEEDBACK_THEME"
|
|
|
|
/**
|
|
* SECTION:fbd-feedback-manager
|
|
* @short_description: The manager processing incoming events
|
|
* @Title: FbdFeedbackManager
|
|
*
|
|
* The #FbdFeedbackManager listens for DBus messages and triggers feedbacks
|
|
* based on the incoming events.
|
|
*/
|
|
|
|
FbdDebugFlags fbd_debug_flags;
|
|
|
|
|
|
typedef struct _FbdFeedbackManager {
|
|
LfbGdbusFeedbackSkeleton parent;
|
|
|
|
GSettings *settings;
|
|
FbdFeedbackProfileLevel level;
|
|
FbdFeedbackTheme *theme;
|
|
guint next_id;
|
|
GStrv allow_important;
|
|
|
|
/* Key: event id, value: event */
|
|
GHashTable *events;
|
|
/* Key: DBus name, value: watch_id */
|
|
GHashTable *clients;
|
|
|
|
/* org.sigxcpu.Feedbackd.Haptic */
|
|
FbdHapticManager *haptic_manager;
|
|
|
|
/* Hardware interaction */
|
|
GUdevClient *client;
|
|
FbdDevVibra *vibra;
|
|
FbdDevSound *sound;
|
|
FbdDevLeds *leds;
|
|
} FbdFeedbackManager;
|
|
|
|
static void fbd_feedback_manager_feedback_iface_init (LfbGdbusFeedbackIface *iface);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (FbdFeedbackManager,
|
|
fbd_feedback_manager,
|
|
LFB_GDBUS_TYPE_FEEDBACK_SKELETON,
|
|
G_IMPLEMENT_INTERFACE (
|
|
LFB_GDBUS_TYPE_FEEDBACK,
|
|
fbd_feedback_manager_feedback_iface_init));
|
|
|
|
static void
|
|
device_changes (FbdFeedbackManager *self, gchar *action, GUdevDevice *device,
|
|
GUdevClient *client)
|
|
{
|
|
g_debug ("Device changes: action = %s, device = %s",
|
|
action, g_udev_device_get_sysfs_path (device));
|
|
|
|
if (g_strcmp0 (action, "remove") == 0 && self->vibra) {
|
|
GUdevDevice *dev = fbd_dev_vibra_get_device (self->vibra);
|
|
|
|
if (g_strcmp0 (g_udev_device_get_sysfs_path (dev),
|
|
g_udev_device_get_sysfs_path (device)) == 0) {
|
|
g_debug ("Vibra device %s got removed", g_udev_device_get_sysfs_path (dev));
|
|
g_clear_object (&self->vibra);
|
|
}
|
|
} else if (g_strcmp0 (action, "add") == 0) {
|
|
if (!g_strcmp0 (g_udev_device_get_property (device, FEEDBACKD_UDEV_ATTR), "vibra")) {
|
|
g_autoptr (GError) err = NULL;
|
|
|
|
g_debug ("Found hotplugged vibra device at %s", g_udev_device_get_sysfs_path (device));
|
|
g_clear_object (&self->vibra);
|
|
self->vibra = fbd_dev_vibra_new (device, &err);
|
|
if (!self->vibra)
|
|
g_warning ("Failed to init vibra device: %s", err->message);
|
|
}
|
|
}
|
|
}
|
|
|
|
static gchar *
|
|
munge_app_id (const gchar *app_id)
|
|
{
|
|
gchar *id = g_strdup (app_id);
|
|
gint i;
|
|
|
|
g_strcanon (id,
|
|
"0123456789"
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
"-",
|
|
'-');
|
|
for (i = 0; id[i] != '\0'; i++)
|
|
id[i] = g_ascii_tolower (id[i]);
|
|
|
|
return id;
|
|
}
|
|
|
|
static FbdFeedbackProfileLevel
|
|
app_get_feedback_level (const gchar *app_id)
|
|
{
|
|
g_autofree gchar *profile = NULL;
|
|
g_autofree gchar *munged_app_id = munge_app_id (app_id);
|
|
g_autofree gchar *path = g_strconcat (APP_PREFIX, munged_app_id, "/", NULL);
|
|
g_autoptr (GSettings) setting = g_settings_new_with_path (APP_SCHEMA, path);
|
|
|
|
profile = g_settings_get_string (setting, FEEDBACKD_KEY_PROFILE);
|
|
g_debug ("%s uses app profile %s", app_id, profile);
|
|
return fbd_feedback_profile_level (profile);
|
|
}
|
|
|
|
static void
|
|
init_devices (FbdFeedbackManager *self)
|
|
{
|
|
GList *l;
|
|
g_autolist (GUdevClient) devices = NULL;
|
|
g_autoptr(GError) err = NULL;
|
|
|
|
devices = g_udev_client_query_by_subsystem (self->client, "input");
|
|
|
|
for (l = devices; l != NULL; l = l->next) {
|
|
GUdevDevice *dev = l->data;
|
|
|
|
if (!g_strcmp0 (g_udev_device_get_property (dev, FEEDBACKD_UDEV_ATTR), "vibra")) {
|
|
g_debug ("Found vibra device");
|
|
self->vibra = fbd_dev_vibra_new (dev, &err);
|
|
if (!self->vibra) {
|
|
g_warning ("Failed to init vibra device: %s", err->message);
|
|
g_clear_error (&err);
|
|
}
|
|
}
|
|
}
|
|
if (!self->vibra)
|
|
g_debug ("No vibra capable device found");
|
|
|
|
self->leds = fbd_dev_leds_new (&err);
|
|
if (!self->leds) {
|
|
g_debug ("Failed to init leds device: %s", err->message);
|
|
g_clear_error (&err);
|
|
}
|
|
|
|
self->sound = fbd_dev_sound_new (&err);
|
|
if (!self->sound) {
|
|
g_warning ("Failed to init sound device: %s", err->message);
|
|
g_clear_error (&err);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_event_feedbacks_ended (FbdFeedbackManager *self, FbdEvent *event)
|
|
{
|
|
guint event_id;
|
|
|
|
g_return_if_fail (FBD_IS_FEEDBACK_MANAGER (self));
|
|
g_return_if_fail (FBD_IS_EVENT (event));
|
|
|
|
event_id = fbd_event_get_id (event);
|
|
event = g_hash_table_lookup (self->events, GUINT_TO_POINTER (event_id));
|
|
if (!event) {
|
|
g_warning ("Feedback ended for unknown event %d", event_id);
|
|
return;
|
|
}
|
|
|
|
g_return_if_fail (fbd_event_get_feedbacks_ended (event));
|
|
|
|
lfb_gdbus_feedback_emit_feedback_ended (LFB_GDBUS_FEEDBACK (self), event_id,
|
|
fbd_event_get_end_reason (event));
|
|
|
|
g_debug ("All feedbacks for event %d finished", event_id);
|
|
g_hash_table_remove (self->events, GUINT_TO_POINTER (event_id));
|
|
}
|
|
|
|
static void
|
|
on_profile_changed (FbdFeedbackManager *self, GParamSpec *psepc, gpointer unused)
|
|
{
|
|
const gchar *pname;
|
|
|
|
g_return_if_fail (FBD_IS_FEEDBACK_MANAGER (self));
|
|
|
|
pname = lfb_gdbus_feedback_get_profile (LFB_GDBUS_FEEDBACK (self));
|
|
|
|
if (!fbd_feedback_manager_set_profile (self, pname))
|
|
g_warning ("Invalid profile '%s'", pname);
|
|
}
|
|
|
|
static void
|
|
on_feedbackd_setting_changed (FbdFeedbackManager *self,
|
|
const gchar *key,
|
|
GSettings *settings)
|
|
{
|
|
g_autofree gchar *profile = NULL;
|
|
|
|
g_return_if_fail (FBD_IS_FEEDBACK_MANAGER (self));
|
|
g_return_if_fail (G_IS_SETTINGS (settings));
|
|
|
|
if (g_str_equal (key, FEEDBACKD_KEY_PROFILE)) {
|
|
profile = g_settings_get_string (settings, key);
|
|
fbd_feedback_manager_set_profile (self, profile);
|
|
} else if (g_str_equal (key, FEEDBACKD_KEY_THEME)) {
|
|
fbd_feedback_manager_load_theme (self);
|
|
} else {
|
|
g_critical ("Unknown settings key '%s'", key);
|
|
}
|
|
}
|
|
|
|
|
|
static gboolean
|
|
app_is_important (FbdFeedbackManager *self, const char *app_id)
|
|
{
|
|
if (!self->allow_important)
|
|
return FALSE;
|
|
|
|
if (g_strv_contains ((const char *const *)self->allow_important, app_id))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
static void
|
|
on_feedbackd_allow_important_changed (FbdFeedbackManager *self,
|
|
const gchar *key,
|
|
GSettings *settings)
|
|
{
|
|
g_return_if_fail (FBD_IS_FEEDBACK_MANAGER (self));
|
|
g_return_if_fail (G_IS_SETTINGS (settings));
|
|
|
|
g_strfreev (self->allow_important);
|
|
self->allow_important = g_settings_get_strv(self->settings, FEEDBACKD_KEY_ALLOW_IMPORTANT);
|
|
}
|
|
|
|
|
|
static void
|
|
on_client_vanished (GDBusConnection *connection,
|
|
const gchar *name,
|
|
gpointer user_data)
|
|
{
|
|
FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (user_data);
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
FbdEvent *event;
|
|
GSList *l;
|
|
g_autoptr (GSList) events = NULL;
|
|
|
|
g_return_if_fail (name);
|
|
|
|
g_debug ("Client %s vanished", name);
|
|
|
|
/*
|
|
* Prepare a list of events to end feedback for so we don't modify
|
|
* the hash table in place when 'feedbacks-ended' fires.
|
|
*/
|
|
g_hash_table_iter_init (&iter, self->events);
|
|
while (g_hash_table_iter_next (&iter, &key, &value)) {
|
|
event = FBD_EVENT (value);
|
|
if (!g_strcmp0 (fbd_event_get_sender (event), name))
|
|
events = g_slist_append (events, event);
|
|
}
|
|
|
|
for (l = events; l; l = l->next) {
|
|
event = l->data;
|
|
g_debug ("Ending event %s (%d) since %s vanished",
|
|
fbd_event_get_event (event),
|
|
fbd_event_get_id (event),
|
|
name);
|
|
fbd_event_end_feedbacks (event);
|
|
}
|
|
|
|
g_hash_table_remove (self->clients, name);
|
|
}
|
|
|
|
static void
|
|
watch_client (FbdFeedbackManager *self, GDBusMethodInvocation *invocation)
|
|
{
|
|
guint watch_id;
|
|
GDBusConnection *conn = g_dbus_method_invocation_get_connection (invocation);
|
|
const char *sender = g_dbus_method_invocation_get_sender (invocation);
|
|
|
|
watch_id = g_bus_watch_name_on_connection (conn,
|
|
sender,
|
|
G_BUS_NAME_WATCHER_FLAGS_NONE,
|
|
NULL,
|
|
on_client_vanished,
|
|
self,
|
|
NULL);
|
|
g_hash_table_insert (self->clients, g_strdup (sender), GUINT_TO_POINTER (watch_id));
|
|
}
|
|
|
|
static void
|
|
free_client_watch (gpointer data)
|
|
{
|
|
guint watch_id = GPOINTER_TO_UINT (data);
|
|
|
|
if (watch_id == 0)
|
|
return;
|
|
g_bus_unwatch_name (watch_id);
|
|
}
|
|
|
|
static FbdFeedbackProfileLevel
|
|
get_max_level (FbdFeedbackProfileLevel global_level,
|
|
FbdFeedbackProfileLevel app_level,
|
|
FbdFeedbackProfileLevel event_level)
|
|
{
|
|
FbdFeedbackProfileLevel level;
|
|
|
|
/* Per app configuration can only select a *lower* feedback level */
|
|
level = MIN (global_level, app_level);
|
|
/* A hint in an event can only select a *lower* feedback level */
|
|
level = MIN (level, event_level);
|
|
|
|
return level;
|
|
}
|
|
|
|
static gboolean
|
|
parse_hints (GVariant *hints,
|
|
FbdFeedbackProfileLevel *level,
|
|
gboolean *hint_important,
|
|
char **hint_sound_file)
|
|
{
|
|
const gchar *profile, *sound_file;
|
|
gboolean found, important;
|
|
g_auto (GVariantDict) dict = G_VARIANT_DICT_INIT (NULL);
|
|
|
|
g_variant_dict_init (&dict, hints);
|
|
|
|
found = g_variant_dict_lookup (&dict, "profile", "&s", &profile);
|
|
if (level && found)
|
|
*level = fbd_feedback_profile_level (profile);
|
|
|
|
found = g_variant_dict_lookup (&dict, "important", "b", &important);
|
|
if (hint_important && found)
|
|
*hint_important = important;
|
|
|
|
found = g_variant_dict_lookup (&dict, "sound-file", "&s", &sound_file);
|
|
if (hint_sound_file && found)
|
|
*hint_sound_file = g_strdup (sound_file);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* add_event_feedbacks:
|
|
*
|
|
* Add the suitable feedacks to the event.
|
|
*
|
|
* Returns: `TRUE` if at least on feedback was added.
|
|
*/
|
|
static gboolean
|
|
add_event_feedbacks (FbdFeedbackManager *self,
|
|
FbdEvent *event,
|
|
GSList *feedbacks,
|
|
FbdFeedbackProfileLevel level,
|
|
const char *sound_file)
|
|
{
|
|
gboolean has_vibra = FALSE, has_sound = FALSE;
|
|
|
|
/* Synthesize sound event for custom sound */
|
|
if (sound_file && level >= FBD_FEEDBACK_PROFILE_LEVEL_FULL) {
|
|
g_autoptr (FbdFeedbackSound) sound = NULL;
|
|
|
|
g_debug ("Using custom sound event '%s'", sound_file);
|
|
sound = fbd_feedback_sound_new_from_file_name (sound_file);
|
|
|
|
fbd_event_add_feedback (event, FBD_FEEDBACK_BASE (sound));
|
|
has_sound = TRUE;
|
|
}
|
|
|
|
for (GSList *l = feedbacks; l; l = l->next) {
|
|
FbdFeedbackBase *fb = FBD_FEEDBACK_BASE (l->data);
|
|
|
|
if (!fbd_feedback_is_available (FBD_FEEDBACK_BASE (fb)))
|
|
continue;
|
|
|
|
if (FBD_IS_FEEDBACK_SOUND (fb) && has_sound)
|
|
continue;
|
|
|
|
/* Handle one haptic feedback at a time. In practice haptics can handle multiple
|
|
* patterns but none of the devices supports this atm */
|
|
/* TODO: should respect priorities */
|
|
if (FBD_IS_FEEDBACK_VIBRA (fb)) {
|
|
if (fbd_dev_vibra_is_busy (self->vibra))
|
|
continue;
|
|
has_vibra = TRUE;
|
|
}
|
|
|
|
/* Events take priority over the haptic interface */
|
|
if (has_vibra)
|
|
fbd_haptic_manager_end_feedback (self->haptic_manager);
|
|
|
|
fbd_event_add_feedback (event, fb);
|
|
}
|
|
|
|
return (fbd_event_get_feedbacks (event) != NULL);
|
|
}
|
|
|
|
|
|
static gboolean
|
|
fbd_feedback_manager_handle_trigger_feedback (LfbGdbusFeedback *object,
|
|
GDBusMethodInvocation *invocation,
|
|
const gchar *arg_app_id,
|
|
const gchar *arg_event,
|
|
GVariant *arg_hints,
|
|
gint arg_timeout)
|
|
{
|
|
FbdFeedbackManager *self;
|
|
FbdEvent *event;
|
|
GSList *feedbacks;
|
|
guint event_id;
|
|
const gchar *sender;
|
|
FbdFeedbackProfileLevel level, hint_level = FBD_FEEDBACK_PROFILE_LEVEL_FULL;
|
|
gboolean found_fb = FALSE;
|
|
gboolean hint_important = FALSE;
|
|
g_autofree char *sound_file = NULL;
|
|
|
|
sender = g_dbus_method_invocation_get_sender (invocation);
|
|
g_debug ("Event '%s' for '%s' from %s", arg_event, arg_app_id, sender);
|
|
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (object), FALSE);
|
|
g_return_val_if_fail (arg_app_id, FALSE);
|
|
g_return_val_if_fail (arg_event, FALSE);
|
|
|
|
self = FBD_FEEDBACK_MANAGER (object);
|
|
if (!strlen (arg_app_id)) {
|
|
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_INVALID_ARGS,
|
|
"Invalid app id %s", arg_app_id);
|
|
return TRUE;
|
|
}
|
|
|
|
if (!strlen (arg_event)) {
|
|
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_INVALID_ARGS,
|
|
"Invalid event %s", arg_event);
|
|
return TRUE;
|
|
}
|
|
|
|
if (!parse_hints (arg_hints, &hint_level, &hint_important, &sound_file)) {
|
|
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
|
|
G_DBUS_ERROR_INVALID_ARGS,
|
|
"Invalid hints");
|
|
return TRUE;
|
|
}
|
|
|
|
if (arg_timeout < -1)
|
|
arg_timeout = -1;
|
|
|
|
event_id = self->next_id++;
|
|
|
|
event = fbd_event_new (event_id, arg_app_id, arg_event, arg_timeout, sender);
|
|
g_hash_table_insert (self->events, GUINT_TO_POINTER (event_id), event);
|
|
|
|
level = fbd_feedback_manager_get_effective_level (self, arg_app_id, hint_level, hint_important);
|
|
|
|
feedbacks = fbd_feedback_theme_lookup_feedback (self->theme, level, event);
|
|
found_fb = add_event_feedbacks (self, event, feedbacks, level, sound_file);
|
|
if (feedbacks)
|
|
g_slist_free_full (feedbacks, g_object_unref);
|
|
|
|
lfb_gdbus_feedback_complete_trigger_feedback (object, invocation, event_id);
|
|
|
|
if (found_fb) {
|
|
g_signal_connect_object (event, "feedbacks-ended",
|
|
(GCallback) on_event_feedbacks_ended,
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
fbd_event_run_feedbacks (event);
|
|
watch_client (self, invocation);
|
|
} else {
|
|
g_hash_table_remove (self->events, GUINT_TO_POINTER (event_id));
|
|
lfb_gdbus_feedback_emit_feedback_ended (LFB_GDBUS_FEEDBACK (self), event_id,
|
|
FBD_EVENT_END_REASON_NOT_FOUND);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fbd_feedback_manager_handle_end_feedback (LfbGdbusFeedback *object,
|
|
GDBusMethodInvocation *invocation,
|
|
guint event_id)
|
|
{
|
|
FbdFeedbackManager *self;
|
|
FbdEvent *event;
|
|
|
|
g_debug ("Ending feedback for event '%d'", event_id);
|
|
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (object), FALSE);
|
|
g_return_val_if_fail (event_id, FALSE);
|
|
|
|
self = FBD_FEEDBACK_MANAGER (object);
|
|
|
|
event = g_hash_table_lookup (self->events, GUINT_TO_POINTER (event_id));
|
|
if (event) {
|
|
/* The last feedback ending will trigger event disposal via
|
|
`on_fb_ended` */
|
|
fbd_event_end_feedbacks (event);
|
|
} else {
|
|
g_warning ("Tried to end non-existing event %d", event_id);
|
|
}
|
|
|
|
lfb_gdbus_feedback_complete_end_feedback (object, invocation);
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
static void
|
|
fbd_feedback_manager_constructed (GObject *object)
|
|
{
|
|
FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (object);
|
|
|
|
G_OBJECT_CLASS (fbd_feedback_manager_parent_class)->constructed (object);
|
|
|
|
g_signal_connect (self, "notify::profile", (GCallback)on_profile_changed, NULL);
|
|
|
|
self->settings = g_settings_new (FEEDBACKD_SCHEMA_ID);
|
|
g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_THEME,
|
|
G_CALLBACK (on_feedbackd_setting_changed), self);
|
|
g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_PROFILE,
|
|
G_CALLBACK (on_feedbackd_setting_changed), self);
|
|
on_feedbackd_setting_changed (self, FEEDBACKD_KEY_PROFILE, self->settings);
|
|
|
|
g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_ALLOW_IMPORTANT,
|
|
G_CALLBACK (on_feedbackd_allow_important_changed), self);
|
|
on_feedbackd_allow_important_changed (self, FEEDBACKD_KEY_ALLOW_IMPORTANT, self->settings);
|
|
|
|
if (self->vibra || fbd_debug_flags & FBD_DEBUG_FLAG_FORCE_HAPTIC)
|
|
self->haptic_manager = fbd_haptic_manager_new ();
|
|
}
|
|
|
|
|
|
static void
|
|
fbd_feedback_manager_dispose (GObject *object)
|
|
{
|
|
FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (object);
|
|
|
|
g_clear_object (&self->haptic_manager);
|
|
|
|
g_clear_object (&self->settings);
|
|
g_clear_object (&self->theme);
|
|
g_clear_object (&self->sound);
|
|
g_clear_object (&self->vibra);
|
|
g_clear_object (&self->leds);
|
|
g_clear_object (&self->client);
|
|
|
|
g_clear_pointer (&self->allow_important, g_strfreev);
|
|
g_clear_pointer (&self->events, g_hash_table_destroy);
|
|
g_clear_pointer (&self->clients, g_hash_table_destroy);
|
|
|
|
G_OBJECT_CLASS (fbd_feedback_manager_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
fbd_feedback_manager_feedback_iface_init (LfbGdbusFeedbackIface *iface)
|
|
{
|
|
iface->handle_trigger_feedback = fbd_feedback_manager_handle_trigger_feedback;
|
|
iface->handle_end_feedback = fbd_feedback_manager_handle_end_feedback;
|
|
}
|
|
|
|
static void
|
|
fbd_feedback_manager_class_init (FbdFeedbackManagerClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->constructed = fbd_feedback_manager_constructed;
|
|
object_class->dispose = fbd_feedback_manager_dispose;
|
|
}
|
|
|
|
static void
|
|
fbd_feedback_manager_init (FbdFeedbackManager *self)
|
|
{
|
|
const gchar * const subsystems[] = { "input", NULL };
|
|
|
|
self->next_id = 1;
|
|
self->level = FBD_FEEDBACK_PROFILE_LEVEL_UNKNOWN;
|
|
|
|
self->client = g_udev_client_new (subsystems);
|
|
g_signal_connect_swapped (G_OBJECT (self->client), "uevent",
|
|
G_CALLBACK (device_changes), self);
|
|
init_devices (self);
|
|
|
|
self->events = g_hash_table_new_full (g_direct_hash,
|
|
g_direct_equal,
|
|
NULL,
|
|
(GDestroyNotify)g_object_unref);
|
|
self->clients = g_hash_table_new_full (g_str_hash,
|
|
g_str_equal,
|
|
g_free,
|
|
free_client_watch);
|
|
}
|
|
|
|
FbdFeedbackManager *
|
|
fbd_feedback_manager_get_default (void)
|
|
{
|
|
static FbdFeedbackManager *instance;
|
|
|
|
if (instance == NULL) {
|
|
instance = g_object_new (FBD_TYPE_FEEDBACK_MANAGER, NULL);
|
|
g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
FbdDevVibra *
|
|
fbd_feedback_manager_get_dev_vibra (FbdFeedbackManager *self)
|
|
{
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (self), NULL);
|
|
|
|
return self->vibra;
|
|
}
|
|
|
|
FbdDevSound *
|
|
fbd_feedback_manager_get_dev_sound (FbdFeedbackManager *self)
|
|
{
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (self), NULL);
|
|
|
|
return self->sound;
|
|
}
|
|
|
|
FbdDevLeds *
|
|
fbd_feedback_manager_get_dev_leds (FbdFeedbackManager *self)
|
|
{
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (self), NULL);
|
|
|
|
return self->leds;
|
|
}
|
|
|
|
void
|
|
fbd_feedback_manager_load_theme (FbdFeedbackManager *self)
|
|
{
|
|
g_autoptr (FbdThemeExpander) expander = NULL;
|
|
g_autoptr (FbdFeedbackTheme) theme = NULL;
|
|
g_autoptr (GError) err = NULL;
|
|
g_auto (GStrv) compatibles = NULL;
|
|
g_autofree char *theme_name = NULL;
|
|
const char *theme_file = g_getenv (FEEDBACKD_THEME_VAR);
|
|
|
|
compatibles = gm_device_tree_get_compatibles (NULL, &err);
|
|
if (compatibles == NULL && err) {
|
|
g_debug ("Failed to get compatibles: %s", err->message);
|
|
g_clear_error (&err);
|
|
}
|
|
|
|
if (theme_file == NULL)
|
|
theme_name = g_settings_get_string (self->settings, FEEDBACKD_KEY_THEME);
|
|
|
|
expander = fbd_theme_expander_new ((const char *const *)compatibles,
|
|
theme_name, theme_file);
|
|
theme = fbd_theme_expander_load_theme_files (expander, &err);
|
|
if (theme) {
|
|
g_set_object(&self->theme, theme);
|
|
} else {
|
|
if (self->theme)
|
|
g_warning ("Failed to reload theme: %s", err->message);
|
|
else
|
|
g_error ("Failed to load any theme: %s", err->message); // No point to carry on
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
cancel_running (FbdFeedbackManager *self)
|
|
{
|
|
g_autoptr (GList) running = g_hash_table_get_values (self->events);
|
|
|
|
for (GList *l = running; l ; l = l->next) {
|
|
FbdEvent *event = FBD_EVENT (l->data);
|
|
|
|
fbd_event_end_feedbacks_by_level (event, self->level);
|
|
}
|
|
}
|
|
|
|
|
|
gboolean
|
|
fbd_feedback_manager_set_profile (FbdFeedbackManager *self, const gchar *profile)
|
|
{
|
|
FbdFeedbackProfileLevel level;
|
|
|
|
g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (self), FALSE);
|
|
|
|
level = fbd_feedback_profile_level (profile);
|
|
|
|
if (level == FBD_FEEDBACK_PROFILE_LEVEL_UNKNOWN)
|
|
return FALSE;
|
|
|
|
if (level == self->level)
|
|
return TRUE;
|
|
|
|
g_debug ("Switching profile to '%s'", profile);
|
|
self->level = level;
|
|
lfb_gdbus_feedback_set_profile (LFB_GDBUS_FEEDBACK (self), profile);
|
|
g_settings_set_string (self->settings, FEEDBACKD_KEY_PROFILE, profile);
|
|
|
|
cancel_running (self);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
FbdHapticManager *
|
|
fbd_feedback_manager_get_haptic_manager (FbdFeedbackManager *self)
|
|
{
|
|
g_assert (FBD_IS_FEEDBACK_MANAGER (self));
|
|
|
|
return self->haptic_manager;
|
|
}
|
|
|
|
/**
|
|
* fbd_feedback_manager_get_effective_level:
|
|
* @self: The feedback manager
|
|
* @app_id: The app-id of the app that triggered the feedback
|
|
* @want_level: The wanted level
|
|
* @important: Whether the important hint is set
|
|
*
|
|
* Calculates the effective feedback level taking the hints sent for the event
|
|
* and the system configuration into account
|
|
*
|
|
* Returns: The effective feedback level
|
|
*/
|
|
FbdFeedbackProfileLevel
|
|
fbd_feedback_manager_get_effective_level (FbdFeedbackManager *self,
|
|
const char *app_id,
|
|
FbdFeedbackProfileLevel want_level,
|
|
gboolean important)
|
|
{
|
|
gboolean can_important;
|
|
FbdFeedbackProfileLevel app_level, level;
|
|
|
|
app_level = app_get_feedback_level (app_id);
|
|
can_important = app_is_important (self, app_id);
|
|
|
|
if (important && can_important)
|
|
level = want_level;
|
|
else
|
|
level = get_max_level (self->level, app_level, want_level);
|
|
|
|
return level;
|
|
}
|