fbd-feedback-manager.c 16.7 KB
Newer Older
Guido Gunther's avatar
Guido Gunther committed
1 2 3 4 5 6 7 8 9 10 11
/*
 * Copyright (C) 2020 Purism SPC
 * 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"
12
#include "fbd-dev-leds.h"
Guido Gunther's avatar
Guido Gunther committed
13 14 15 16 17 18 19 20 21 22 23
#include "fbd-event.h"
#include "fbd-feedback-vibra.h"
#include "fbd-feedback-manager.h"
#include "fbd-feedback-theme.h"

#include <gio/gio.h>
#include <glib-unix.h>
#include <gudev/gudev.h>

#define FEEDBACKD_SCHEMA_ID "org.sigxcpu.feedbackd"
#define FEEDBACKD_KEY_PROFILE "profile"
24
#define FEEDBACKD_THEME_VAR "FEEDBACK_THEME"
Guido Gunther's avatar
Guido Gunther committed
25

26 27 28
#define APP_SCHEMA FEEDBACKD_SCHEMA_ID ".application"
#define APP_PREFIX "/org/sigxcpu/feedbackd/application/"

29 30 31
#define DEVICE_TREE_PATH "/sys/firmware/devicetree/base/compatible"
#define DEVICE_NAME_MAX 1024

32

Guido Gunther's avatar
Guido Gunther committed
33 34 35 36 37 38 39 40 41 42 43 44
/**
 * 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.
 */

typedef struct _FbdFeedbackManager {
  LfbGdbusFeedbackSkeleton parent;

45 46 47 48
  GSettings               *settings;
  FbdFeedbackProfileLevel  level;
  FbdFeedbackTheme        *theme;
  guint                    next_id;
Guido Gunther's avatar
Guido Gunther committed
49

50
  GHashTable              *events;
Guido Gunther's avatar
Guido Gunther committed
51 52

  /* Hardware interaction */
53 54 55 56
  GUdevClient             *client;
  FbdDevVibra             *vibra;
  FbdDevSound             *sound;
  FbdDevLeds              *leds;
Guido Gunther's avatar
Guido Gunther committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
} 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);

78
    if (g_strcmp0 (g_udev_device_get_sysfs_path (dev),
79
                   g_udev_device_get_sysfs_path (device)) == 0) {
80
      g_debug ("Vibra device %s got removed", g_udev_device_get_sysfs_path (dev));
Guido Gunther's avatar
Guido Gunther committed
81 82
      g_clear_object (&self->vibra);
    }
83
  } else if (g_strcmp0 (action, "add") == 0) {
Guido Gunther's avatar
Guido Gunther committed
84 85 86 87
    if (!g_strcmp0 (g_udev_device_get_property (device, "FEEDBACKD_TYPE"), "vibra")) {
      g_autoptr (GError) err = NULL;

      g_debug ("Found hotplugged vibra device at %s", g_udev_device_get_sysfs_path (device));
88
      g_clear_object (&self->vibra);
Guido Gunther's avatar
Guido Gunther committed
89
      self->vibra = fbd_dev_vibra_new (device, &err);
90
      if (!self->vibra)
91
        g_warning ("Failed to init vibra device: %s", err->message);
Guido Gunther's avatar
Guido Gunther committed
92 93 94 95
    }
  }
}

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
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);
}

Guido Gunther's avatar
Guido Gunther committed
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
static void
init_devices (FbdFeedbackManager *self)
{
  GList *devices, *l;
  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_TYPE"), "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");

150 151 152 153 154 155
  self->leds = fbd_dev_leds_new (&err);
  if (!self->leds) {
    g_debug ("Failed to init leds device: %s", err->message);
    g_clear_error (&err);
  }

Guido Gunther's avatar
Guido Gunther committed
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
  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));

180
  lfb_gdbus_feedback_emit_feedback_ended (LFB_GDBUS_FEEDBACK (self), event_id,
181
                                          fbd_event_get_end_reason (event));
Guido Gunther's avatar
Guido Gunther committed
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216

  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);

  /* TODO: end running feedbacks that aren't allowed in new profile immediately */
}

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));
  g_return_if_fail (!g_strcmp0 (key, FEEDBACKD_KEY_PROFILE));

  profile = g_settings_get_string (settings, key);
  fbd_feedback_manager_set_profile (self, profile);
}

217 218 219 220 221 222 223 224 225 226 227 228 229
static FbdFeedbackProfileLevel
get_max_level (FbdFeedbackProfileLevel global_level,
               FbdFeedbackProfileLevel app_level,
               FbdFeedbackProfileLevel event_level)
{
  FbdFeedbackProfileLevel level;

  /* Individual events and apps can select lower levels than the global level but not higher ones */
  level = global_level > app_level ? app_level : global_level;
  level = level > event_level ? event_level : level;
  return level;
}

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
static gboolean
parse_hints (GVariant *hints, FbdFeedbackProfileLevel *level)
{
  const gchar *profile;
  gboolean found;
  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);
  return TRUE;
}

Guido Gunther's avatar
Guido Gunther committed
245
static gboolean
246
fbd_feedback_manager_handle_trigger_feedback (LfbGdbusFeedback      *object,
Guido Gunther's avatar
Guido Gunther committed
247
                                              GDBusMethodInvocation *invocation,
248 249 250 251
                                              const gchar           *arg_app_id,
                                              const gchar           *arg_event,
                                              GVariant              *arg_hints,
                                              gint                   arg_timeout)
Guido Gunther's avatar
Guido Gunther committed
252 253 254 255 256
{
  FbdFeedbackManager *self;
  FbdEvent *event;
  GSList *feedbacks, *l;
  gint event_id;
257
  const gchar *sender;
258
  FbdFeedbackProfileLevel app_level, level, hint_level = FBD_FEEDBACK_PROFILE_LEVEL_FULL;
259
  gboolean found_fb = FALSE;
Guido Gunther's avatar
Guido Gunther committed
260

261 262
  sender = g_dbus_method_invocation_get_sender (invocation);
  g_debug ("Event '%s' for '%s' from %s", arg_event, arg_app_id, sender);
Guido Gunther's avatar
Guido Gunther committed
263 264 265 266 267 268 269

  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)) {
270 271 272
    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                           G_DBUS_ERROR_INVALID_ARGS,
                                           "Invalid app id %s", arg_app_id);
Guido Gunther's avatar
Guido Gunther committed
273 274 275 276 277 278 279 280 281 282
    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;
  }

283 284 285 286 287 288 289
  if (!parse_hints (arg_hints, &hint_level)) {
    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                           G_DBUS_ERROR_INVALID_ARGS,
                                           "Invalid hints");
    return TRUE;
  }

Guido Gunther's avatar
Guido Gunther committed
290 291 292 293 294
  if (arg_timeout < -1)
    arg_timeout = -1;

  event_id = self->next_id++;

295
  event = fbd_event_new (event_id, arg_app_id, arg_event, arg_timeout, sender);
Guido Gunther's avatar
Guido Gunther committed
296 297
  g_hash_table_insert (self->events, GUINT_TO_POINTER (event_id), event);

298
  app_level = app_get_feedback_level (arg_app_id);
299
  level = get_max_level (self->level, app_level, hint_level);
Guido Gunther's avatar
Guido Gunther committed
300

301
  feedbacks = fbd_feedback_theme_lookup_feedback (self->theme, level, event);
Guido Gunther's avatar
Guido Gunther committed
302 303 304
  if (feedbacks) {
    for (l = feedbacks; l; l = l->next) {
      FbdFeedbackBase *fb = l->data;
305 306

      if (fbd_feedback_is_available (FBD_FEEDBACK_BASE (fb))) {
307 308
        fbd_event_add_feedback (event, fb);
        found_fb = TRUE;
309
      }
Guido Gunther's avatar
Guido Gunther committed
310
    }
311
    g_slist_free_full (feedbacks, g_object_unref);
312 313 314 315
  } else {
    /* No feedbacks found at all */
    found_fb = FALSE;
  }
Guido Gunther's avatar
Guido Gunther committed
316

317
  if (found_fb) {
Guido Gunther's avatar
Guido Gunther committed
318
    g_signal_connect_object (event, "feedbacks-ended",
319 320 321
                             (GCallback) on_event_feedbacks_ended,
                             self,
                             G_CONNECT_SWAPPED);
Guido Gunther's avatar
Guido Gunther committed
322 323
    fbd_event_run_feedbacks (event);
  } else {
324
    g_hash_table_remove (self->events, GUINT_TO_POINTER (event_id));
325
    lfb_gdbus_feedback_emit_feedback_ended (LFB_GDBUS_FEEDBACK (self), event_id,
326
                                            FBD_EVENT_END_REASON_NOT_FOUND);
Guido Gunther's avatar
Guido Gunther committed
327 328 329 330 331 332 333
  }

  lfb_gdbus_feedback_complete_trigger_feedback (object, invocation, event_id);
  return TRUE;
}

static gboolean
334
fbd_feedback_manager_handle_end_feedback (LfbGdbusFeedback      *object,
Guido Gunther's avatar
Guido Gunther committed
335
                                          GDBusMethodInvocation *invocation,
336
                                          guint                  event_id)
Guido Gunther's avatar
Guido Gunther committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
{
  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;
}

361
static const gchar *
362 363
find_themefile (void)
{
364 365 366
  gint i = 0;
  gsize len;
  const gchar *comp;
367 368 369

  g_autoptr (GError) err = NULL;
  gchar **xdg_data_dirs = (gchar **) g_get_system_data_dirs ();
370 371 372 373
  g_autofree gchar *config_path = NULL;
  g_autofree gchar *compatibles = NULL;

  // Try to read the device name
374 375
  if (g_file_test (DEVICE_TREE_PATH, (G_FILE_TEST_EXISTS))) {
    g_debug ("Found device tree device compatible at %s", DEVICE_TREE_PATH);
376 377

    // Check if feedbackd has a proper config available this device
378
    if (!g_file_get_contents (DEVICE_TREE_PATH, &compatibles, &len, &err))
379 380 381
      g_warning ("Unable to read: %s", err->message);

    comp = compatibles;
382
    while (comp - compatibles < len) {
383 384

      // Iterate over $XDG_DATA_DIRS
385 386 387
      for (i = 0; i < g_strv_length (xdg_data_dirs); i++) {
        config_path = g_strconcat (xdg_data_dirs[i], "feedbackd/themes/", comp, ".json", NULL);
        g_debug ("Searching for device specific themefile in %s", config_path);
388 389

        // Check if file exist
390 391 392
        if (g_file_test (config_path, (G_FILE_TEST_EXISTS))) {
          g_debug ("Found themefile for this device at: %s", config_path);
          return g_strdup (config_path);
393 394 395 396
        }
      }

      // Next compatible
397
      comp = strchr (comp, 0);
398 399
      comp++;
    }
400 401
  }else  {
    g_debug ("Device tree path does not exist: %s", DEVICE_TREE_PATH);
402 403 404 405 406
  }

  return NULL;
}

Guido Gunther's avatar
Guido Gunther committed
407 408 409 410 411 412 413 414 415
static void
fbd_feedback_manager_constructed (GObject *object)
{
  g_autoptr (GError) err = NULL;
  FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (object);
  const gchar *themefile;

  G_OBJECT_CLASS (fbd_feedback_manager_parent_class)->constructed (object);

416
  // Overide themefile with environment variable if requested
417
  themefile = g_getenv (FEEDBACKD_THEME_VAR);
418 419 420 421 422 423

  // Search for device-specific configuration
  if (!themefile)
    themefile = find_themefile ();

  // Fallback to default configuration if needed
Guido Gunther's avatar
Guido Gunther committed
424 425
  if (!themefile)
    themefile = FEEDBACKD_THEME_DIR "/default.json";
426
  g_info ("Using themefile: %s", themefile);
Guido Gunther's avatar
Guido Gunther committed
427 428 429 430

  self->theme = fbd_feedback_theme_new_from_file (themefile, &err);
  if (!self->theme) {
    /* No point to carry on */
431
    g_error ("Failed to load theme: %s", err->message);
Guido Gunther's avatar
Guido Gunther committed
432 433 434 435
  }

  g_signal_connect (self, "notify::profile", (GCallback)on_profile_changed, NULL);
  lfb_gdbus_feedback_set_profile (LFB_GDBUS_FEEDBACK (self),
436
                                  fbd_feedback_profile_level_to_string (self->level));
Guido Gunther's avatar
Guido Gunther committed
437 438 439 440 441 442 443 444 445 446 447 448 449 450

  self->settings = g_settings_new (FEEDBACKD_SCHEMA_ID);
  g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_PROFILE,
                            G_CALLBACK (on_feedbackd_setting_changed), self);
}

static void
fbd_feedback_manager_dispose (GObject *object)
{
  FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (object);

  g_clear_object (&self->settings);
  g_clear_object (&self->theme);
  g_clear_object (&self->vibra);
451
  g_clear_object (&self->leds);
Guido Gunther's avatar
Guido Gunther committed
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
  g_clear_object (&self->client);
  g_clear_pointer (&self->events, 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_FULL;

  self->client = g_udev_client_new (subsystems);
  g_signal_connect_swapped (G_OBJECT (self->client), "uevent",
484
                            G_CALLBACK (device_changes), self);
Guido Gunther's avatar
Guido Gunther committed
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
  init_devices (self);

  self->events = g_hash_table_new_full (g_direct_hash,
                                        g_direct_equal,
                                        NULL,
                                        (GDestroyNotify)g_object_unref);
}

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;
}

521 522 523 524 525 526 527 528
FbdDevLeds *
fbd_feedback_manager_get_dev_leds (FbdFeedbackManager *self)
{
  g_return_val_if_fail (FBD_IS_FEEDBACK_MANAGER (self), NULL);

  return self->leds;
}

Guido Gunther's avatar
Guido Gunther committed
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
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);
  return TRUE;
}