From 3663b7d1495ec8dbdafca5ddb4fedea9e7525465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Fri, 14 Feb 2020 17:38:57 +0100 Subject: [PATCH] Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guido Günther --- .editorconfig | 35 + .gitignore | 14 + .gitlab-ci.yml | 81 +++ COPYING | 674 ++++++++++++++++++ COPYING.LIB | 502 +++++++++++++ Event-naming-spec-0.0.0.md | 89 +++ Feedback-theme-spec-0.0.0.md | 74 ++ README.md | 135 ++++ cli/fbcli.c | 158 ++++ cli/meson.build | 16 + data/default.json | 128 ++++ data/meson.build | 65 ++ data/org.sigxcpu.Feedback.service.in | 4 + data/org.sigxcpu.Feedback.xml | 67 ++ data/org.sigxcpu.feedbackd.gschema.xml | 18 + debian/README.source | 29 + debian/changelog | 5 + debian/control | 74 ++ debian/copyright | 41 ++ debian/feedbackd.install | 5 + debian/feedbackd.udev | 9 + debian/gbp.conf | 4 + debian/gir1.2-feedback-0.0.install | 1 + debian/libfeedback-0.0-0.install | 1 + debian/libfeedback-dev.install | 6 + debian/rules | 18 + debian/source/format | 1 + debian/tests/build-test | 9 + debian/tests/build-test.c | 9 + debian/tests/control | 13 + doc/build-howto.xml | 89 +++ doc/libfeedback-docs.xml | 73 ++ doc/meson.build | 40 ++ doc/xml/gtkdocentities.ent.in | 9 + doc/xml/meson.build | 11 + examples/example.py | 11 + gir1.2-feedback-0.0.install | 1 + libfeedback/lfb-event.c | 617 ++++++++++++++++ libfeedback/lfb-event.h | 80 +++ libfeedback/lfb-names.h | 12 + libfeedback/lfb-priv.h | 16 + libfeedback/libfeedback.c | 200 ++++++ libfeedback/libfeedback.h | 31 + libfeedback/meson.build | 109 +++ meson.build | 119 ++++ meson_options.txt | 12 + run.in | 12 + src/fbd-dev-sound.c | 131 ++++ src/fbd-dev-sound.h | 26 + src/fbd-dev-vibra.c | 367 ++++++++++ src/fbd-dev-vibra.h | 26 + src/fbd-event.c | 449 ++++++++++++ src/fbd-event.h | 52 ++ src/fbd-feedback-base.c | 229 ++++++ src/fbd-feedback-base.h | 31 + src/fbd-feedback-dummy.c | 128 ++++ src/fbd-feedback-dummy.h | 16 + src/fbd-feedback-manager.c | 394 ++++++++++ src/fbd-feedback-manager.h | 24 + src/fbd-feedback-profile.c | 341 +++++++++ src/fbd-feedback-profile.h | 35 + src/fbd-feedback-sound.c | 142 ++++ src/fbd-feedback-sound.h | 18 + src/fbd-feedback-theme.c | 319 +++++++++ src/fbd-feedback-theme.h | 34 + src/fbd-feedback-vibra-periodic.c | 156 ++++ src/fbd-feedback-vibra-periodic.h | 18 + src/fbd-feedback-vibra-rumble.c | 175 +++++ src/fbd-feedback-vibra-rumble.h | 18 + src/fbd-feedback-vibra.c | 157 ++++ src/fbd-feedback-vibra.h | 28 + src/fbd.c | 119 ++++ src/fbd.h | 13 + src/meson.build | 68 ++ tests/data/test.json | 36 + tests/meson.build | 86 +++ tests/services/meson.build | 9 + .../services/org.sigxcpu.Feedback.service.in | 4 + tests/test-fbd-event.c | 168 +++++ tests/test-fbd-feedback-profile.c | 94 +++ tests/test-fbd-feedback-theme.c | 124 ++++ tests/test-lfb-event.c | 69 ++ tests/test-lfb-integration.c | 228 ++++++ 83 files changed, 8059 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 COPYING create mode 100644 COPYING.LIB create mode 100644 Event-naming-spec-0.0.0.md create mode 100644 Feedback-theme-spec-0.0.0.md create mode 100644 README.md create mode 100644 cli/fbcli.c create mode 100644 cli/meson.build create mode 100644 data/default.json create mode 100644 data/meson.build create mode 100644 data/org.sigxcpu.Feedback.service.in create mode 100644 data/org.sigxcpu.Feedback.xml create mode 100644 data/org.sigxcpu.feedbackd.gschema.xml create mode 100644 debian/README.source create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/feedbackd.install create mode 100644 debian/feedbackd.udev create mode 100644 debian/gbp.conf create mode 100644 debian/gir1.2-feedback-0.0.install create mode 100644 debian/libfeedback-0.0-0.install create mode 100644 debian/libfeedback-dev.install create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100755 debian/tests/build-test create mode 100644 debian/tests/build-test.c create mode 100644 debian/tests/control create mode 100644 doc/build-howto.xml create mode 100644 doc/libfeedback-docs.xml create mode 100644 doc/meson.build create mode 100644 doc/xml/gtkdocentities.ent.in create mode 100644 doc/xml/meson.build create mode 100755 examples/example.py create mode 100644 gir1.2-feedback-0.0.install create mode 100644 libfeedback/lfb-event.c create mode 100644 libfeedback/lfb-event.h create mode 100644 libfeedback/lfb-names.h create mode 100644 libfeedback/lfb-priv.h create mode 100644 libfeedback/libfeedback.c create mode 100644 libfeedback/libfeedback.h create mode 100644 libfeedback/meson.build create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100755 run.in create mode 100644 src/fbd-dev-sound.c create mode 100644 src/fbd-dev-sound.h create mode 100644 src/fbd-dev-vibra.c create mode 100644 src/fbd-dev-vibra.h create mode 100644 src/fbd-event.c create mode 100644 src/fbd-event.h create mode 100644 src/fbd-feedback-base.c create mode 100644 src/fbd-feedback-base.h create mode 100644 src/fbd-feedback-dummy.c create mode 100644 src/fbd-feedback-dummy.h create mode 100644 src/fbd-feedback-manager.c create mode 100644 src/fbd-feedback-manager.h create mode 100644 src/fbd-feedback-profile.c create mode 100644 src/fbd-feedback-profile.h create mode 100644 src/fbd-feedback-sound.c create mode 100644 src/fbd-feedback-sound.h create mode 100644 src/fbd-feedback-theme.c create mode 100644 src/fbd-feedback-theme.h create mode 100644 src/fbd-feedback-vibra-periodic.c create mode 100644 src/fbd-feedback-vibra-periodic.h create mode 100644 src/fbd-feedback-vibra-rumble.c create mode 100644 src/fbd-feedback-vibra-rumble.h create mode 100644 src/fbd-feedback-vibra.c create mode 100644 src/fbd-feedback-vibra.h create mode 100644 src/fbd.c create mode 100644 src/fbd.h create mode 100644 src/meson.build create mode 100644 tests/data/test.json create mode 100644 tests/meson.build create mode 100644 tests/services/meson.build create mode 100644 tests/services/org.sigxcpu.Feedback.service.in create mode 100644 tests/test-fbd-event.c create mode 100644 tests/test-fbd-feedback-profile.c create mode 100644 tests/test-fbd-feedback-theme.c create mode 100644 tests/test-lfb-event.c create mode 100644 tests/test-lfb-integration.c diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4f7618f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[meson.build] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.{c,h,h.in}] +indent_size = 2 +tab_size = 2 +indent_style = space +max_line_length = 80 + +[*.css] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.xml] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.json] +indent_size = 2 +tab_size = 2 +indent_style = space + +[NEWS] +max_line_length = 72 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a81aac9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +_build +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/gir1.2-feedback-0.0.substvars +debian/gir1.2-feedback-0.0/ +debian/feedbackd.debhelper.log +debian/feedbackd.substvars +debian/feedbackd/ +debian/libfeedback-0.0-0.substvars +debian/libfeedback-0.0-0/ +debian/libfeedback-dev.substvars +debian/libfeedback-dev/ +debian/tmp/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..19780bf --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,81 @@ +include: + - 'https://source.puri.sm/Librem5/librem5-ci/raw/master/librem5-pipeline-definitions.yml' + +stages: + - build + - test + - package + - test-package + +variables: + DEPS: build-essential git + +before_script: + - export DEBIAN_FRONTEND=noninteractive + - apt-get -y update + - apt-get -y build-dep . + - apt-get -y install gcovr + +.tags: &tags + tags: + - librem5 + +# Regular, full build +build:native-debian-buster:full: + <<: *tags + stage: build + image: debian:buster + artifacts: + paths: + - _build + script: + - meson --werror . _build + - ninja -C _build + +# Minimal build for e.g. submodule usage +build:native-debian-buster:lib: + <<: *tags + stage: build + image: debian:buster + script: + - meson --werror -Ddaemon=false -Dgtk_doc=false . _build + - ninja -C _build + +build:native-debian-bullseye: + <<: *tags + stage: build + image: debian:bullseye + artifacts: + paths: + - _build + script: + - meson -Db_coverage=true --werror . _build + - ninja -C _build + +test:native-debian-bullseye: + <<: *tags + stage: test + image: debian:bullseye + dependencies: + - build:native-debian-bullseye + coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/' + script: + - dbus-run-session ninja -C _build test coverage + +package:deb-debian-buster: + extends: .l5-build-debian-package + +package:deb-debian-buster:arm64: + tags: + - librem5:arm64 + extends: .l5-build-debian-package + +autopkgtest-debian-buster-package: + dependencies: + - package:deb-debian-buster + extends: .l5-autopkgtest-debian-package + +lintian-debian-buster-package: + dependencies: + - package:deb-debian-buster + extends: .l5-lintian-debian-package diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/COPYING.LIB b/COPYING.LIB new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/COPYING.LIB @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/Event-naming-spec-0.0.0.md b/Event-naming-spec-0.0.0.md new file mode 100644 index 0000000..2c5fcd7 --- /dev/null +++ b/Event-naming-spec-0.0.0.md @@ -0,0 +1,89 @@ +This is similar in spirit (and heavily based on) to the [Sound naming spec][]. + +# Overview + +This specification gives direction on how to name the Event types +(triggering feedbacks such as running the haptic motor or blinking an +LED) that are available for use by applications, when creating a +feedback theme. It does so by laying out a standard naming scheme for +Event creation, as well as providing a minimal list of must have +Events, and a larger list with many more examples to help with the +creation of extended Events for third party applications, with +different event types and usage. + +# Context + +The list of default contexts for the feedback theme are: + +- Alerts: Events to alert the user of an action or event which may + have a major impact on the system or their current use +- Notifications: Events to trigger feedback to notify the user that + the system, or their current use case has changed state in some way, + e.g. new email arriving +- Actions: Event that notify the user on their actions. +- Input Event: This triggers feedbacks that give direct response to + input events from the user, such as key presses on an on screen + keyboard + +# Event naming guides + +Here we define some guidelines for when creating new Event names +that extend the standardized list of Event names defined here, in +order to provide Events for more specific events and usages. + +Event names are in the en_US.US_ASCII locale. This means that the +characters allowed in the Event names must fall within the US-ASCII +character set. As a further restriction, all Event names may only +contain lowercase letters, numbers, underscore, dash, or period +characters. Spaces, colons, slashes, and backslashes are not +allowed. Also, sound names must be spelled as they are in the en_US +dictionary. + +Events for branded applications should be named the same as the binary +executable for the application, prefixed by the string “x-”, to avoid +name space clashes with future standardized names. Example: +“x-openoffice-foobar”. + +## Standard Event names + +This section describes the standard Event names that should be used +by artists when creating themes, and by developers when writing +applications which will use the Feedback Theme Specification. + +### Alerts + +- battery-low: The Event used when the battery is low (below 20%, for example). +- power-unplug-battery-low: The power cable has been unplugged and the battery level is low. + +### Notifications + +- message-new-instant: The event used when a new IM is received. +- message-new-sms: The event used when a new sms is received. +- message-new-email: The event used when a new email is received. +- message-missed-email: The event used when an email was received but not seen by the user. +- message-missed-instant: The event used when a instant message was received but not seen by the user. +- message-missed-sms: The event used when a sms message was received but not seen by the user. +- phone-incoming-call: This Event is used when a phone/voip call is coming in. +- phone-missed-call: This Event is used when a phone/voip call is was incoming but not answered. +- battery-caution: The event used when the battery is nearing exhaustion (below 40%, for example) +- battery-full: The event used when the battery is fully loaded up. +- device-added: The event used when a device has become available to the desktop, i.e. due to USB plugging. +- power-plug: The power cable has been plugged in. +- power-unplug: The power cable has been unplugged. +- alarm-clock-elapsed: A user configured alarm elapsed. + +### Actions + +- message-sent-instant: The sound used when a new IM is sent. +- bell-terminal: The sound to use as a terminal bell. +- theme-demo: A event that should be played for demoing this theme. Usually + this should just be an alias for a very representative sound (such as + a incoming phone call) of a theme that would work nicely as a demo event for + a theme in the theme selector dialog. + +### Input Event + +- button-pressed: The Event used when a button is pressed. +- window-close: The sound used when an existing window is closed. + +[Sound naming spec]: http://0pointer.de/public/sound-naming-spec.html diff --git a/Feedback-theme-spec-0.0.0.md b/Feedback-theme-spec-0.0.0.md new file mode 100644 index 0000000..2b80d85 --- /dev/null +++ b/Feedback-theme-spec-0.0.0.md @@ -0,0 +1,74 @@ +# Overview + +A feedback theme defines the kind of feedback that events (as +described in the [Event naming spec][]) will trigger. + +The feedback daemon is responsible to select an appropriate feedback +theme for a given device. + +# Definitions + +- Feedback: A feedback is something that notifies the user that + s.th. happened (e.g. a played sound, the vibration of a haptic + motor or led blinking). + +- Feedback theme: A feedback theme is a set of feedbacks grouped by + profiles. Each feedback is mapped to a single event. + +- Feedback Profile: A feedback profile groups feedbacks by + "noisiness". The currently defined profiles names are + *full*, *quiet* and *silent*. With *full* being the noisiest + profile. + +- Event: What the user should be notified about. The event + names are built according to the [Event naming spec][]. + +# Implementation + +When an application triggers feedback for an event in the feedback +daemon the daemon selects the provided feedback like this: + +1. feedbacks from a nosier profiles like the currently selected are + ignored + +2. All other feedbacks for this event are selected and run to provide + feedback to the user + +With the above a feedback theme in YAML format could look like: + +```yaml +full: + - event-name: phone-incoming-call + type: Sound + effect: phone-incoing-call + - event-name: message-new-sms + ... +quiet: + - event-name: phone-incoming-call + type: Vibra + duration: 0.5s + - event-name: message-new-sms + ... +silent: + - event-name: phone-incoming-call + type: Led + location: Front + Color: Green + Interval: 0.2s + - event-name: message-new-sms + ... +``` + +At the time of writing the theme format is daemon dependent. E.g. feedbackd +uses a format [similar to the above in JSON](./data/default.json). + + +# Recommendations + +- The silent theme shoud not produce any audible feedback. This includes + the buzzing of haptic motors. +- The quiet theme should not play any sounds. Haptic motors and LEDs can + be used. +- The full feedback theme can use any available feedback mechanisms + +[Event naming spec]: ./Event-naming-spec-0.0.0.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e0e0d0 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +Haptic/visual/audio feedback for GNOME +====================================== +[![Code coverage](https://source.puri.sm/Librem5/feedbackd/badges/master/coverage.svg)](https://source.puri.sm/guido.gunther/feedbackd/commits/master) + +feedbackd provides a DBus daemon (feedbackd) to act on events to provide +haptic, visual and audio feedback. It offers a library (libfeedback) and +GObject introspection bindings to ease using it from applications. + +## License + +feedbackd is licensed under the GPLv3+ while the libfeedback library is +licensed under LGPL 2.1+. + +## Getting the source + +```sh +git clone https://source.puri.sm/Librem5/feedbackd +cd feedbackd +``` + +The master branch has the current development version. + +## Dependencies +On a Debian based system run + +```sh +sudo apt-get -y install build-essential +sudo apt-get -y build-dep . +``` + +For an explicit list of dependencies check the `Build-Depends` entry in the +[debian/control][] file. + +## Building + +We use the meson (and thereby Ninja) build system for feedbackd. The quickest +way to get going is to do the following: + + meson . _build + ninja -C _build + ninja -C _build test + ninja -C _build install + +## Running +### Running from the source tree +To run the daemon use + +```sh +_build/run +``` +You can introspect and get the current theme with + +```sh +gdbus introspect --session --dest org.sigxcpu.Feedback --object-path /org/sigxcpu/Feedback +``` + +and to request feedback for an event + +```sh +gdbus call --session --dest org.sigxcpu.Feedback --object-path /org/sigxcpu/Feedback --method org.sigxcpu.Feedback.Feedback 'my.app.id' 'phone-incoming-call' '[]' 0 +``` + +See `examples/` for a simple python example using GObject introspection. + +# How it works + +We're using a [event naming spec](./Event-naming-spec-0.0.0.md) +similar to http://0pointer.de/public/sound-naming-spec.html to name +events. This will allow us to act as a system sound library so +applications only need to call into this library and things like +the quiet and silent profile work out of the box. + +## Feedback theme +Events are then mapped to a specific type of feedback (sound, led, vibra) via a +device specific theme (since devices have different capabilities). + +There's currently only a single hardcoded theme named `default`. The currently +available feedback types are: + +- Sound (an audible sound from the sound naming spec) +- VibraRumble: haptic motor rumbling +- VibraPeriodic: periodic feedback from the haptic motor + +You can check the feedback theme and the classes (prefixed with Fbd) +for available properties. Note that the feedback theme API (including +the theme file format) is not stable but considered internal to the +daemon. + +## Profiles +The profile determines which parts of the theme are in use: + +- `full`: Use conigured events form the `full`, `quiet` and `silent` parts of + the feedback them. +- `quiet`: Use `quiet` and `silent` part from of the feedback theme. This usually + means no audio feedback. +- `silent`: Only use the `silent` part from the feedback theme. This usually means + to not use audio or vibra. + +It can be set via a GSetting + +```sh + gsettings set org.sigxcpu.feedbackd profile full +``` +## fbdcli + +`fbdcli` can be used to trigger feedback for different events. Here are some examples: + +### Phone call +Run feedbacks for event `phone-incoming-call` until explicitly stopped: + +``` +_build/cli/fbcli -t 0 -E phone-incoming-call +``` + +### New instant message +Run feedbacks for event `message-new-instant` just once: + +``` +_build/cli/fbcli -t -1 -E message-new-instant +``` + +### Alarm clock +Run feedbacks for event `message-new-instant` for 10 seconds: + +``` +_build/cli/fbcli -t 10 -E alarm-clock-elapsed +``` + +# Documentation + +- [Libfeedback API](https://honk.sigxcpu.org/projects/feedbackd/doc/) +- [Event naming spec draft](./Event-naming-spec-0.0.0.md) +- [Feedback-theme-spec draft](./Feedback-theme-spec-0.0.0.md) + +[debian/control]: ./debian/control#L5 diff --git a/cli/fbcli.c b/cli/fbcli.c new file mode 100644 index 0000000..d4cb312 --- /dev/null +++ b/cli/fbcli.c @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ +#define LIBFEEDBACK_USE_UNSTABLE_API +#include "libfeedback.h" + +#include +#include + +#include + +#define DEFAULT_EVENT "phone-incoming-call" + +static GMainLoop *loop; + +static gboolean +on_watch_expired (gpointer unused) +{ + g_warning ("Watch expired waiting for all feedbacks to finish"); + g_main_loop_quit (loop); + + return G_SOURCE_REMOVE; +} + +static void +on_feedback_ended (LfbEvent *event, int* data) +{ + g_return_if_fail (LFB_IS_EVENT (event)); + + g_debug ("Feedback ended for event"); + *data = TRUE; + g_main_loop_quit (loop); +} + +static gboolean +on_user_input (GIOChannel *channel, GIOCondition cond, LfbEvent *event) +{ + g_autoptr(GError) err = NULL; + + if (cond == G_IO_IN) { + g_print ("Ending feedback\n"); + if (!lfb_event_end_feedback (event, &err)) + g_warning("Failed to end feedback: %s", err->message); + } + return FALSE; +} + +static gboolean +trigger_event(const char *name, gint timeout) +{ + g_autoptr(GError) err = NULL; + g_autoptr(LfbEvent) event = NULL; + g_autoptr(GIOChannel) input = NULL; + int success = FALSE; + + g_print("Triggering feedback for event '%s'\n", name); + event = lfb_event_new (name); + lfb_event_set_timeout (event, timeout); + + g_signal_connect (event, "feedback-ended", (GCallback)on_feedback_ended, &success); + if (!lfb_event_trigger_feedback (event, &err)) { + g_print("Failed to report event: %s\n", err->message); + return FALSE; + } + + input = g_io_channel_unix_new (STDIN_FILENO); + g_io_add_watch (input, G_IO_IN, (GIOFunc)on_user_input, event); + g_print ("Press to end feedback right away.\n"); + + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + g_main_loop_unref (loop); + + return success; +} + +static void +on_profile_changed (LfbGdbusFeedback *proxy, GParamSpec *psepc, gpointer unused) +{ + g_print("Set feedback profile to: '%s'\n", + lfb_get_feedback_profile()); + g_main_loop_quit (loop); +} + +static gboolean +set_profile (const gchar *profile) +{ + LfbGdbusFeedback *proxy; + const gchar *current; + + current = lfb_get_feedback_profile(); + g_debug("Current profile is %s", current); + if (!g_strcmp0 (current, profile)) { + g_print ("Profile is already set to %s\n", profile); + return TRUE; + } + + g_debug("Setting profile to %s", profile); + proxy = lfb_get_proxy (); + + /* Set profile and wait until we got notified about the profile change */ + loop = g_main_loop_new (NULL, FALSE); + lfb_set_feedback_profile(profile); + g_signal_connect (proxy, "notify::profile", (GCallback)on_profile_changed, NULL); + g_main_loop_run (loop); + g_print("Current feedback profile is: '%s'\n", + lfb_get_feedback_profile()); + return TRUE; +} + +int main(int argc, char *argv[0]) +{ + g_autoptr(GOptionContext) opt_context = NULL; + g_autoptr(GError) err = NULL; + g_autofree gchar *profile = NULL; + const char *name = NULL; + gboolean success; + int watch = 30; + int timeout = -1; + const GOptionEntry options [] = { + {"event", 'E', 0, G_OPTION_ARG_STRING, &name, + "Event name. (default: " DEFAULT_EVENT ").", NULL}, + {"timeout", 't', 0, G_OPTION_ARG_INT, &timeout, + "Run feedback for timeout seconds", NULL}, + {"profile", 'P', 0, G_OPTION_ARG_STRING, &profile, + "Profile name to set" , NULL}, + {"watch", 'w', 0, G_OPTION_ARG_INT, &watch, + "How long to watch for feedback longest", NULL}, + { NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL } + }; + + setlocale (LC_ALL, ""); + opt_context = g_option_context_new ("- A cli for feedbackd"); + g_option_context_add_main_entries (opt_context, options, NULL); + if (!g_option_context_parse (opt_context, &argc, &argv, &err)) { + g_warning ("%s", err->message); + return 1; + } + + if (!lfb_init("org.sigxcpu.fbcli", &err)) { + g_print("Failed to init libfeedback: %s\n", err->message); + return 1; + } + + if (!name) + name = g_strdup (DEFAULT_EVENT); + + g_timeout_add_seconds (watch, (GSourceFunc)on_watch_expired, NULL); + if (profile) + success = set_profile (profile); + else + success = trigger_event (name, timeout); + + lfb_uninit (); + return !success; +} diff --git a/cli/meson.build b/cli/meson.build new file mode 100644 index 0000000..e542e1c --- /dev/null +++ b/cli/meson.build @@ -0,0 +1,16 @@ +fbcli_sources = [ + 'fbcli.c', +] + +fbcli_deps = [ + libfeedback_dep, + gio, + gio_unix, +] + +executable( + 'fbcli', + fbcli_sources, + dependencies: fbcli_deps, + install: true, +) diff --git a/data/default.json b/data/default.json new file mode 100644 index 0000000..a335e75 --- /dev/null +++ b/data/default.json @@ -0,0 +1,128 @@ +{ + "name" : "default", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "event-name" : "phone-incoming-call", + "type" : "Sound", + "effect" : "phone-incoming-call" + }, + { + "event-name" : "message-new-instant", + "type" : "Sound", + "effect" : "message-new-instant" + }, + { + "event-name" : "message-new-sms", + "type" : "Sound", + "effect" : "message-new-instant" + }, + { + "event-name" : "message-new-email", + "type" : "Sound", + "effect" : "message-new-email" + }, + { + "event-name" : "alarm-clock-elapsed", + "type" : "Sound", + "effect" : "alarm-clock-elapsed" + }, + { + "event-name" : "theme-demo", + "type" : "Sound", + "effect" : "theme-demo" + } + ] + }, + { + "name" : "quiet", + "feedbacks" : [ + { + "event-name" : "phone-incoming-call", + "type" : "VibraPeriodic", + "duration" : 10000 + }, + { + "event-name" : "phone-missed-call", + "type" : "VibraRumble", + "duration" : 300 + }, + { + "event-name" : "message-new-instant", + "type" : "VibraRumble", + "duration" : 150 + }, + { + "event-name" : "message-new-sms", + "type" : "VibraRumble", + "duration" : 500, + "count" : 2, + "pause" : 100 + }, + { + "event-name" : "message-new-email", + "type" : "VibraRumble", + "duration" : 750, + "count" : 3, + "pause" : 100 + }, + { + "event-name" : "message-sent-instant", + "type" : "VibraRumble", + "duration" : 150 + }, + { + "event-name" : "bell-terminal", + "type" : "VibraRumble", + "duration" : 100 + }, + { + "event-name" : "alarm-clock-elapsed", + "type" : "VibraPeriodic", + "magnitude" : 32767, + "duration" : 10000, + "fade-in-time" : 10000, + "fade-in-level": 4096 + }, + { + "event-name" : "teme-demo", + "type" : "VibraPeriodic", + "magnitude" : 32767, + "duration" : 5000 + } + ] + }, + { + "name" : "silent", + "feedbacks" : [ + { + "event-name" : "phone-missed-call", + "type" : "Dummy", + "duration" : 10000 + }, + { + "event-name" : "message-missed-email", + "type" : "Dummy", + "duration" : 10000 + }, + { + "event-name" : "message-missed-instant", + "type" : "Dummy", + "duration" : 10000 + }, + { + "event-name" : "message-missed-sms", + "type" : "Dummy", + "duration" : 10000 + }, + { + "event-name" : "battery-caution", + "type" : "Dummy", + "duration" : 10000 + } + ] + } + ] +} diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..d90230e --- /dev/null +++ b/data/meson.build @@ -0,0 +1,65 @@ +# DBus server interface +prefix = 'org.sigxcpu.' +namespace = 'LfbGdbus' + +generated_dbus_sources = [] + +dbus_interfaces = [ 'org.sigxcpu.Feedback.xml' ] + +generated_dbus_sources += gnome.gdbus_codegen('lfb-gdbus', + sources : dbus_interfaces, + object_manager : false, + docbook : 'libfeedback', + interface_prefix : prefix, + install_header : true, + install_dir : join_paths(get_option('includedir'), libname), + namespace : namespace) + +dbus_header_dir = meson.current_build_dir() +dbus_inc = include_directories('.') +install_data(dbus_interfaces, install_dir: ifacedir) + +schemas = ['org.sigxcpu.feedbackd.gschema.xml'] +compiled = gnome.compile_schemas( + build_by_default: true +) + +install_data( + schemas, + install_dir: 'share/glib-2.0/schemas' +) + + +if get_option('daemon') + +service_data = configuration_data() +service_data.set('libexecdir', libexecdir) +service_file = 'org.sigxcpu.Feedback.service' + +configure_file( + input : service_file + '.in', + output : service_file, + configuration : service_data, + install : true, + install_dir: servicedir, +) + +theme_json = ['default.json'] +install_data( + theme_json, + install_dir: feedbackd_theme_dir, +) + +if json_glib_validate.found() + jsons = [] + foreach theme : theme_json + jsons += (join_paths(meson.current_source_dir(), theme_json)) + endforeach + custom_target('validate-json', + build_by_default: true, + input: theme_json, + output: 'json_validate_stamp', + command: [json_glib_validate.path()] + jsons) +endif + +endif # daemon diff --git a/data/org.sigxcpu.Feedback.service.in b/data/org.sigxcpu.Feedback.service.in new file mode 100644 index 0000000..16e92c7 --- /dev/null +++ b/data/org.sigxcpu.Feedback.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.sigxcpu.Feedback +Exec=@libexecdir@/feedbackd + diff --git a/data/org.sigxcpu.Feedback.xml b/data/org.sigxcpu.Feedback.xml new file mode 100644 index 0000000..90b0ec8 --- /dev/null +++ b/data/org.sigxcpu.Feedback.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.sigxcpu.feedbackd.gschema.xml b/data/org.sigxcpu.feedbackd.gschema.xml new file mode 100644 index 0000000..927f5a6 --- /dev/null +++ b/data/org.sigxcpu.feedbackd.gschema.xml @@ -0,0 +1,18 @@ + + + + + + + + + 'full' + Feedack profile to use + + The feedback profile determines the audio and haptical feedback + level given on events to the user. + + + + diff --git a/debian/README.source b/debian/README.source new file mode 100644 index 0000000..90e1f00 --- /dev/null +++ b/debian/README.source @@ -0,0 +1,29 @@ +This package is maintained with git-buildpackage(1). It follows DEP-14 +for branch naming (e.g. using debian/sid for the current version +in Debian unstable). + +It uses pristine-tar(1) to store enough information in git to generate +bit identical tarballs when building the package without having +downloaded an upstream tarball first. + +When working with patches it is recommended to use "gbp pq import" to +import the patches, modify the source and then use "gbp pq export +--commit" to commit the modifications. + +The changelog is generated using "gbp dch" so if you submit any +changes don't bother to add changelog entries but rather provide +a nice git commit message that can then end up in the changelog. + +It is recommended to build the package with pbuilder using: + + gbp buildpackage --git-pbuilder + +For information on how to set up a pbuilder environment see the +git-pbuilder(1) manpage. In short: + + DIST=sid git-pbuilder create + gbp clone + cd + gbp buildpackage --git-pbuilder + + -- Guido Günther , Wed, 2 Dec 2015 18:51:15 +0100 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..09be134 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +feedbackd (0.0.0+git20200214) amber-phone; urgency=medium + + * Initial git snapshot + + -- Guido Günther Wed, 16 Oct 2019 21:22:33 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..6be5d17 --- /dev/null +++ b/debian/control @@ -0,0 +1,74 @@ +Source: feedbackd +Section: x11 +Priority: optional +Maintainer: Guido Günther +Build-Depends: + dbus, + debhelper-compat (= 12), + dh-sequence-gir, + gtk-doc-tools, + libgirepository1.0-dev, + libglib2.0-dev, + libgudev-1.0-dev (>= 232), + libgsound-dev, + libjson-glib-dev, + meson, + pkg-config, + valac, +Standards-Version: 4.4.0 +Homepage: https://source.puri.sm/Librem5/feedbackd + +Package: feedbackd +Architecture: any +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + sound-theme-freedesktop, +Description: DBus service for haptic/visual/audio feedback + Feedbackd is a DBus activated daemon that provides haptic/ + visual/audio feedback based on events. + . + This package contains the daemon. + +Package: libfeedback-0.0-0 +Architecture: any +Section: libs +Depends: + ${misc:Depends}, + ${shlibs:Depends}, +Description: DBus service for haptic/visual/audio feedback + Feedbackd is a DBus activated daemon that provides haptic/ + visual/audio feedback based on events. + . + This package contains the shared libraries for applications interfacing with + Feedbackd and the API documentation. + +Package: libfeedback-dev +Architecture: any +Section: libdevel +Depends: + gir1.2-feedback-0.0 (= ${binary:Version}), + libfeedback-0.0-0 (= ${binary:Version}), + libglib2.0-dev, + ${misc:Depends}, + ${shlibs:Depends}, +Description: DBus service for haptic/visual/audio feedback + Feedbackd is a DBus activated daemon that provides haptic/ + visual/audio feedback based on events. + . + This package contains development files and Vala bindings to use when writing + applications that interface with GThd. + +Package: gir1.2-feedback-0.0 +Architecture: any +Section: introspection +Depends: + ${gir:Depends}, + ${misc:Depends}, + ${shlibs:Depends}, +Description: GObject introspection data for libfeedback + Feedbackd is a DBus activated daemon that provides haptic/ + visual/audio feedback based on events. + . + This package contains introspection data for the libfeedback library. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..115b353 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,41 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: feedbackd +Source: https://source.puri.sm/Librem5/feedbackd + +Files: * +Copyright: 2020 Purism SPC +License: GPL-3+ + This package 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. + . + This package 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 this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + +Files: libfeedback/* +Copyright: 2020 Purism SPC +License: LGPL-2.1+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + . + This package 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 Lesser General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU Lesser General + Public License version 3 can be found in "/usr/share/common-licenses/LGPL-2.1". diff --git a/debian/feedbackd.install b/debian/feedbackd.install new file mode 100644 index 0000000..05ec6b0 --- /dev/null +++ b/debian/feedbackd.install @@ -0,0 +1,5 @@ +/usr/bin/ +/usr/libexec/ +/usr/share/dbus-1/ +/usr/share/feedbackd/ +/usr/share/glib-2.0/schemas diff --git a/debian/feedbackd.udev b/debian/feedbackd.udev new file mode 100644 index 0000000..c0ab3d8 --- /dev/null +++ b/debian/feedbackd.udev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of feedbackd. + +ACTION=="remove", GOTO="feedbackd_end" + +SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT}=="1", ENV{ID_PATH}=="platform-vibrator", TAG+="uaccess", ENV{FEEDBACKD_TYPE}="vibra" + +LABEL="feedbackd_end" diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..ce77267 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +debian-branch = master +debian-tag = v%(version)s +debian-tag-msg = %(pkg)s v%(version)s diff --git a/debian/gir1.2-feedback-0.0.install b/debian/gir1.2-feedback-0.0.install new file mode 100644 index 0000000..066de77 --- /dev/null +++ b/debian/gir1.2-feedback-0.0.install @@ -0,0 +1 @@ +/usr/lib/*/girepository-1.0 diff --git a/debian/libfeedback-0.0-0.install b/debian/libfeedback-0.0-0.install new file mode 100644 index 0000000..4b44156 --- /dev/null +++ b/debian/libfeedback-0.0-0.install @@ -0,0 +1 @@ +/usr/lib/*/libfeedback-*.so.0 diff --git a/debian/libfeedback-dev.install b/debian/libfeedback-dev.install new file mode 100644 index 0000000..51ccd17 --- /dev/null +++ b/debian/libfeedback-dev.install @@ -0,0 +1,6 @@ +/usr/include +/usr/share/gir-1.0/ +/usr/share/gtk-doc/ +/usr/share/vala/ +/usr/lib/*/libfeedback-*.so +/usr/lib/*/pkgconfig/libfeedback-*.pc diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..89eb79e --- /dev/null +++ b/debian/rules @@ -0,0 +1,18 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +%: + dh $@ --builddirectory=_build + +override_dh_auto_configure: + dh_auto_configure -- -Dgtk_doc=true + +override_dh_installudev: + # Make sure all the symlink rules in 60 ran: + dh_installudev --priority=90 + +override_dh_auto_test: +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + dbus-run-session ninja -C _build test +endif diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/tests/build-test b/debian/tests/build-test new file mode 100755 index 0000000..7d5bbda --- /dev/null +++ b/debian/tests/build-test @@ -0,0 +1,9 @@ +#!/usr/bin/make -f + +CFLAGS=$(shell pkg-config --cflags libfeedback-0.0) +LIBS=$(shell pkg-config --libs libfeedback-0.0) + +a.out: debian/tests/build-test.c + gcc $(CFLAGS) $< $(LIBS) + @echo "Build test of $< succeeded" + @rm -f a.out diff --git a/debian/tests/build-test.c b/debian/tests/build-test.c new file mode 100644 index 0000000..d1623dd --- /dev/null +++ b/debian/tests/build-test.c @@ -0,0 +1,9 @@ +#define LIBFEEDBACK_USE_UNSTABLE_API +#include + +int +main (int argc, + char **argv) +{ + lfb_init("org.sigxcpu.autopkgtest1", NULL); +} diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..9d3ee38 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,13 @@ +Tests: build-test +Depends: + build-essential, + libfeedback-dev, + pkg-config, +Restrictions: allow-stderr, superficial + +Test-Command: python3 -c 'import gi; from gi.repository import Lfb' +Depends: + gir1.2-feedback-0.0, + python3, + python3-gi, +Restrictions: allow-stderr, superficial diff --git a/doc/build-howto.xml b/doc/build-howto.xml new file mode 100644 index 0000000..c22c78a --- /dev/null +++ b/doc/build-howto.xml @@ -0,0 +1,89 @@ + + + + %gtkdocentities; +]> + + + + Compiling with &package_string; + 3 + + + + Compiling with &package_string;Notes on compiling + + + + Using pkg-config + + Like other GNOME libraries, + &package_string; uses + pkg-config to provide compiler options. The + package name is + "&package_ver_str;". So in + your configure.ac script, you might specify something + like: + + + PKG_CHECK_MODULES(LIBFEEDBACK, [&package_string;-&package_api_version;]) + AC_SUBST(LIBFEEDBACK_CFLAGS) + AC_SUBST(LIBFEEDBACK_LIBS) + + + + Or if using Meson/Ninja use a dependency('&package_string;-&package_api_version;') + dependency. + + + + The "&package_api_version;" in the package name is the "API version" + (indicating "the version of the &package_string; API + that first appeared in version &package_api_version;") and is essentially just part of + the package name. + + + + When using the Meson build system you can declare a dependency like + + + dependency(libfeedback-0.0') + + + + + + Acknowledge the Instability + + + Since the library is young and is still changing a lot, in order to use it you are required to acknowledge that your are using an unstable API. + To do so, LIBFEEDBACK_USE_UNSTABLE_API must be defined for compilation to succeed. + + + + From C code or any compatible language, you can prefix your inclusion of the &package_string; header like so: + + + + #define LIBFEEDBACK_USE_UNSTABLE_API + #include <libfeedback.h> + + + + Including individual headers rather than libfeedback.h is not + recommended. + + + You can also acknoledge this with the definition option of your C compiler, like -DFEEDBACK_USE_UNSTABLE_API. + This is required from Vala. + + + + To use &package_name; from Vala, you must define the acknowledgment in C via -X -DFEEDBACK_USE_UNSTABLE_API. + If your build system uses a two pass compilation and hence your Vala compiler outputs C (Meson, Automake, or using the --ccode Vala compiler option trigger that) then you must add -DLIBFEEDBACK_USE_UNSTABLE_API to your C compiler argments instead. + + + + diff --git a/doc/libfeedback-docs.xml b/doc/libfeedback-docs.xml new file mode 100644 index 0000000..ae2460e --- /dev/null +++ b/doc/libfeedback-docs.xml @@ -0,0 +1,73 @@ + + + + %gtkdocentities; +]> + + + &package_name; Reference Manual + + This document is the API reference for &package_name; &package_version; + + Libfeedback is a library to trigger + feedback (such as haptic feedback) via events sent by your application. + + + If you find any issues in this API reference, please report it using + the bugtracker. + + + + + 2020 + Purism SPC + + + + + Introduction + + + The purpose of libfeedback is to trigger audible, haptic and visual feedback based on + events. Events are identified by strings. + + + + + + + Core API + + + + + + Low level api + + + + + + DBus Interface + + + + + Object Hierarchy + + + + + API Index + + + + + Annotations glossary + + + + diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..5fc9fd5 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,40 @@ +if get_option('gtk_doc') + +subdir('xml') + +private_headers = [ +] + +content_files = [ + 'build-howto.xml', +] + +glib_prefix = dependency('glib-2.0').get_pkgconfig_variable('prefix') +glib_docpath = join_paths(glib_prefix, 'share', 'gtk-doc', 'html') +docpath = join_paths(get_option('datadir'), 'gtk-doc', 'html') + +gnome.gtkdoc('libfeedback', + main_xml: 'libfeedback-docs.xml', + src_dir: [ + join_paths(meson.source_root(), 'libfeedback'), + join_paths(meson.build_root(), 'libfeedback'), + join_paths(meson.build_root(), 'data'), + ], + dependencies: libfeedback_dep, + gobject_typesfile: 'libfeedback.types', + scan_args: [ + '--rebuild-types', + '--ignore-headers=' + ' '.join(private_headers), + ], + fixxref_args: [ + '--html-dir=@0@'.format(docpath), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'glib')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gobject')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gio')), + '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gi')), + ], + install_dir: 'libfeedback', + content_files: content_files, + install: true) + +endif diff --git a/doc/xml/gtkdocentities.ent.in b/doc/xml/gtkdocentities.ent.in new file mode 100644 index 0000000..db8f29d --- /dev/null +++ b/doc/xml/gtkdocentities.ent.in @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/doc/xml/meson.build b/doc/xml/meson.build new file mode 100644 index 0000000..6de270e --- /dev/null +++ b/doc/xml/meson.build @@ -0,0 +1,11 @@ +ent_conf = configuration_data() +ent_conf.set('PACKAGE', 'libfeedback') +ent_conf.set('PACKAGE_BUGREPORT', 'https://source.puri.sm/Librem5/feedbackd/issues') +ent_conf.set('PACKAGE_NAME', 'libfeedback') +ent_conf.set('PACKAGE_STRING', 'libfeedback') +ent_conf.set('PACKAGE_TARNAME', 'libfeedback-' + meson.project_version()) +ent_conf.set('PACKAGE_URL', 'https://source.puri.sm/Librem5/feedbackd') +ent_conf.set('PACKAGE_VERSION', meson.project_version()) +ent_conf.set('PACKAGE_API_VERSION', apiversion) +configure_file(input: 'gtkdocentities.ent.in', output: 'gtkdocentities.ent', configuration: ent_conf) + diff --git a/examples/example.py b/examples/example.py new file mode 100755 index 0000000..b97fec8 --- /dev/null +++ b/examples/example.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# +# export GI_TYPELIB_PATH=/usr/local/lib/x86_64-linux-gnu/girepository-1.0/ + +import gi +gi.require_version('Lfb', '0.0') +from gi.repository import Lfb + +Lfb.init('org.sigxcpu.lfbexample') +event = Lfb.Event.new('phone-incoming-call') +event.trigger() diff --git a/gir1.2-feedback-0.0.install b/gir1.2-feedback-0.0.install new file mode 100644 index 0000000..066de77 --- /dev/null +++ b/gir1.2-feedback-0.0.install @@ -0,0 +1 @@ +/usr/lib/*/girepository-1.0 diff --git a/libfeedback/lfb-event.c b/libfeedback/lfb-event.c new file mode 100644 index 0000000..637c09c --- /dev/null +++ b/libfeedback/lfb-event.c @@ -0,0 +1,617 @@ +/* + * 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 several 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 feedback can be triggered like + * + * |[ + * g_autoptr (GError) err = NULL; + * LpfEvent *event = lfb_event_new ("message-new-instant"); + * ret = lfb_event_trigger_feedback (event, &err); + * ]| + * + * 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 (). + * Since these methods involve DBus calls there are asynchronous variants + * available, see e.g. #lfb_event_trigger_feedback_async(). + */ + +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_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 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; + + /** + * LpfEvent: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); + + /** + * LpfEvent: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. + */ + 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; +} + +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); + 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); + 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; +} diff --git a/libfeedback/lfb-event.h b/libfeedback/lfb-event.h new file mode 100644 index 0000000..00992fb --- /dev/null +++ b/libfeedback/lfb-event.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include +#include + +#if !defined (__LIBFEEDBACK_H_INSIDE__) && !defined (LIBFEEDBACK_COMPILATION) +#error "Only can be included directly." +#endif + +G_BEGIN_DECLS + +/** + * LfbEventState: + * @LFB_EVENT_STATE_ERRORED: An error occured triggering feedbacks + * @LFB_EVENT_STATE_NONE: No state information yet + * @LFB_EVENT_STATE_RUNNING: The feedbacks for this event are currently running + * @LFB_EVENT_STATE_ENDED: All feedbacks for this event ended + * + * Enum values to indicate the current state of the feedbacks + * triggered by an event. + */ + +typedef enum _LfbEventState { + LFB_EVENT_STATE_ERRORED = -1, + LFB_EVENT_STATE_NONE = 0, + LFB_EVENT_STATE_RUNNING = 1, + LFB_EVENT_STATE_ENDED = 2, +} LfbEventState; + +/** + * LfbEventEndReason: + * @LFB_EVENT_END_REASON_NOT_FOUND: There was no feedback in the current theme for this event + * so no feedback was provided to the user. + * @LFB_EVENT_END_REASON_NATURAL: All feedbacks finished playing their natural length + * @LFB_EVENT_END_REASON_EXPIRED: Feedbacks ran until the set timeout expired + * @LFB_EVENT_END_REASON_EXPLICIT: The feedbacks were ended explicitly + * + * Enum values used to indicate why the feedbacks for an event ended. + **/ +typedef enum _LfbEventEndReason { + + LFB_EVENT_END_REASON_NOT_FOUND = -1, + LFB_EVENT_END_REASON_NATURAL = 0, + LFB_EVENT_END_REASON_EXPIRED = 1, + LFB_EVENT_END_REASON_EXPLICIT = 2, +} LfbEventEndReason; + +#define LFB_TYPE_EVENT (lfb_event_get_type()) + +G_DECLARE_FINAL_TYPE (LfbEvent, lfb_event, LFB, EVENT, GObject); + +LfbEvent* lfb_event_new (const char *event); +gboolean lfb_event_trigger_feedback (LfbEvent *self, GError **error); +void lfb_event_trigger_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean lfb_event_trigger_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error); +gboolean lfb_event_end_feedback (LfbEvent *self, GError **error); +void lfb_event_end_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean lfb_event_end_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error); +void lfb_event_set_timeout (LfbEvent *self, gint timeout); +gint lfb_event_get_timeout (LfbEvent *self); +const char *lfb_event_get_event (LfbEvent *self); +LfbEventState lfb_event_get_state (LfbEvent *self); +LfbEventEndReason lfb_event_get_end_reason (LfbEvent *self); + +G_END_DECLS diff --git a/libfeedback/lfb-names.h b/libfeedback/lfb-names.h new file mode 100644 index 0000000..16b8f4f --- /dev/null +++ b/libfeedback/lfb-names.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#define FB_DBUS_NAME "org.sigxcpu.Feedback" +#define FB_DBUS_PATH "/org/sigxcpu/Feedback" + +#define FB_DBUS_TYPE G_BUS_TYPE_SESSION diff --git a/libfeedback/lfb-priv.h b/libfeedback/lfb-priv.h new file mode 100644 index 0000000..b03fb0f --- /dev/null +++ b/libfeedback/lfb-priv.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include +#include "lfb-gdbus.h" + +G_BEGIN_DECLS + +LfbGdbusFeedback *_lfb_get_proxy (void); + +G_END_DECLS diff --git a/libfeedback/libfeedback.c b/libfeedback/libfeedback.c new file mode 100644 index 0000000..6adba3e --- /dev/null +++ b/libfeedback/libfeedback.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: LGPL-2.1+ + * Author: Guido Günther + */ +#include "libfeedback.h" +#include "lfb-gdbus.h" +#include "lfb-priv.h" + +#include "lfb-names.h" + +/** + * SECTION:libfeedback + * @Short_description: Library initialization and helpers + * @Title: libfeedback + * + * To use the library call #lfb_init() with the id of your application + * (usually the desktop file name without the .desktop extension). + * When done call #lfb_uninit() to free any resources. + * + * |[ + * #define LIBFEEDBACK_USE_UNSTABLE_API + * #include + * + * int main(void) + * { + * g_autoptr (GError) *err = NULL; + * if (lfb_init ("com.example.appid", &err)) { + * g_error ("%s", err->message); + * } + * // run your application, emit events + * // ... + * // when done: + * lfb_uninit (); + * return 0; + * } + * ]| + */ + +static LfbGdbusFeedback *_proxy; +static char *_app_id; +static gboolean _initted; + +LfbGdbusFeedback * +_lfb_get_proxy (void) +{ + /* Caller needs to check since the proxy might be gone when + the last event gets finalized */ + return _proxy; +} + +/** + * lfb_init: + * @app_id: The application id + * @error: Error information + * + * Initialize libfeedback. This must be called before any other functions. + * + * Returns: %TRUE if successful, or %FALSE on error. + */ +gboolean +lfb_init (const gchar *app_id, GError **error) +{ + g_return_val_if_fail (app_id != NULL, FALSE); + g_return_val_if_fail (*app_id != '\0', FALSE); + + if (_initted) + return TRUE; + + lfb_set_app_id (app_id); + _proxy = lfb_gdbus_feedback_proxy_new_for_bus_sync( + FB_DBUS_TYPE, 0, FB_DBUS_NAME, FB_DBUS_PATH, NULL, error); + if (!_proxy) + return FALSE; + + g_object_add_weak_pointer (G_OBJECT (_proxy), (gpointer *) &_proxy); + + _initted = TRUE; + return TRUE; +} + +/** + * lfb_uninit: + * + * Uninitialize the library when no longer used. Usually called + * on program shutdown. + */ +void +lfb_uninit (void) +{ + g_clear_pointer (&_app_id, g_free); + g_clear_object (&_proxy); + _initted = FALSE; +} + +/** + * lfb_set_app_id: + * @app_id: The application id + * + * Sets the application id. + */ +void +lfb_set_app_id (const char *app_id) +{ + g_free (_app_id); + _app_id = g_strdup (app_id); +} + +/** + * lfb_get_app_id: + * + * Returns: the application id. + */ +const gchar * +lfb_get_app_id (void) +{ + return _app_id; +} + +/** + * lfb_is_initted: + * + * Gets whether or not libfeedback is initialized. + * + * Returns: %TRUE if libfeedback is initialized, or %FALSE otherwise. + */ +gboolean +lfb_is_initted (void) +{ + return _initted; +} + +/** + * lfb_get_feedback_profile: + * + * Gets the currently set feedback profile. + * + * Returns: The current profile or %NULL on error. + */ +const char * +lfb_get_feedback_profile (void) +{ + LfbGdbusFeedback *proxy; + g_autofree gchar *profile = NULL; + g_autoptr(GVariant) var = NULL; + + if (!lfb_is_initted ()) { + g_warning ("you must call lfb_init() before getting the profile"); + g_assert_not_reached (); + } + + proxy = _lfb_get_proxy (); + g_return_val_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy), NULL); + + return lfb_gdbus_feedback_get_profile (LFB_GDBUS_FEEDBACK (proxy)); +} + + +/** + * lfb_set_feedback_profile: + * @profile: The profile to set + * + * Sets the active feedback profile to #profile. It is up to the feedback + * daemon to ignore this request. The new profile might not become active + * immediately. You can listen to changes #LfbGdbusFeedback's ::profile + * property to get notified when it takes effect. + */ +void +lfb_set_feedback_profile (const gchar *profile) +{ + LfbGdbusFeedback *proxy; + + if (!lfb_is_initted ()) { + g_warning ("you must call lfb_init() before setting the profile"); + g_assert_not_reached (); + } + + proxy = _lfb_get_proxy (); + g_return_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy)); + + lfb_gdbus_feedback_set_profile (LFB_GDBUS_FEEDBACK (proxy), profile); +} + +/** + * lfb_get_proxy: + * + * This can be used to access the lower level API e.g. to listen to + * property changes. The object is not owned by the caller. Don't + * unref it after use. + * + * Returns: (transfer none): The DBus proxy. + */ +LfbGdbusFeedback * +lfb_get_proxy (void) +{ + LfbGdbusFeedback *proxy = _lfb_get_proxy (); + + g_return_val_if_fail (LFB_GDBUS_IS_FEEDBACK (proxy), NULL); + return proxy; +} diff --git a/libfeedback/libfeedback.h b/libfeedback/libfeedback.h new file mode 100644 index 0000000..15a0591 --- /dev/null +++ b/libfeedback/libfeedback.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ +#pragma once + +#include + +G_BEGIN_DECLS + +#define __LIBFEEDBACK_H_INSIDE__ + +#ifndef LIBFEEDBACK_USE_UNSTABLE_API +#error LIBFEEDBACK is unstable API. You must define LIBFEEDBACK_USE_UNSTABLE_API before including libfeedback.h +#endif + +#include "lfb-enums.h" +#include "lfb-event.h" +#include "lfb-gdbus.h" + +gboolean lfb_init (const gchar *app_id, GError **error); +void lfb_uninit (void); +void lfb_set_app_id (const char *app_id); +const char *lfb_get_app_id (void); +gboolean lfb_is_initted (void); +void lfb_set_feedback_profile (const char *profile); +const char *lfb_get_feedback_profile (void); +LfbGdbusFeedback *lfb_get_proxy (void); + +G_END_DECLS diff --git a/libfeedback/meson.build b/libfeedback/meson.build new file mode 100644 index 0000000..c5239cf --- /dev/null +++ b/libfeedback/meson.build @@ -0,0 +1,109 @@ +# Shared library and language bindings + +libfeedback_libdir = get_option('libdir') +libfeedback_header_dir = get_option('includedir') / libname + +lfb_enum_headers = files([ + 'lfb-event.h', +]) +lfb_enum_sources = gnome.mkenums_simple('lfb-enums', + sources : lfb_enum_headers, + install_header : true, + install_dir: libfeedback_header_dir, +) + +libfeedback_headers = [ + 'libfeedback.h', + 'lfb-event.h', +] + +libfeedback_sources = [ + generated_dbus_sources, + lfb_enum_sources, + libfeedback_headers, + 'libfeedback.c', + 'lfb-event.c', +] + +libfeedback_deps = [ + gio, + gio_unix, + glib, +] + +libfeedback_c_args = [ + '-DG_LOG_DOMAIN="libfeedback"', + '-DFEEDBACK_COMPILATION', + '-DLIBFEEDBACK_USE_UNSTABLE_API', +] + +libtype = 'shared_library' +libfeedback = build_target( + 'feedback-' + apiversion, + libfeedback_sources, + soversion : 0, + c_args : libfeedback_c_args, + dependencies : libfeedback_deps, + install : true, + install_dir : libfeedback_libdir, + target_type : libtype, +) + +libfeedback_inc = include_directories('.') +libfeedback_dep = declare_dependency( + sources : [generated_dbus_sources, lfb_enum_sources], + link_with : libfeedback, + dependencies: libfeedback_deps, + include_directories : libfeedback_inc, +) +install_headers(libfeedback_headers, subdir : libname) + +pkgconfig = import('pkgconfig') + +pkgconfig.generate( + libraries : [libfeedback], + subdirs : libname, + version : meson.project_version(), + name : 'Feedback', + filebase : 'libfeedback-@0@'.format(apiversion), + description : 'GNOME feedback library', + requires : 'gio-2.0', + install_dir : join_paths(libfeedback_libdir, 'pkgconfig'), + ) + +if introspection + libhandy_gir_extra_args = [ + '-I' + dbus_header_dir, + '--c-include=libfeedback.h', + '--quiet', + '-DLIBFEEDBACK_COMPILATION', + '-DLIBFEEDBACK_USE_UNSTABLE_API', + '--warn-error', + '--warn-all', + ] + + libfeedback_gir = gnome.generate_gir(libfeedback, + sources : libfeedback_sources, + nsversion : apiversion, + namespace : 'Lfb', + export_packages : 'libfeedback-@0@'.format(apiversion), + symbol_prefix : 'lfb', + identifier_prefix : 'Lfb', + link_with : libfeedback, + includes : ['Gio-2.0'], + install : true, + install_dir_gir : girdir, + install_dir_typelib : typelibdir, + extra_args : libhandy_gir_extra_args, + ) + + if get_option('vapi') + libfeedback_vapi = gnome.generate_vapi('libfeedback-' + apiversion, + sources : libfeedback_gir[0], + packages : ['gio-2.0'], + install : true, + install_dir : vapidir, + metadata_dirs : [meson.current_source_dir()], + ) + endif +endif diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..bc76f23 --- /dev/null +++ b/meson.build @@ -0,0 +1,119 @@ +project('feedbackd', 'c', + version : '0.0.0', + license : 'LGPLv3+', + meson_version : '>= 0.49.0', + default_options : [ + 'warning_level=1', + 'buildtype=debugoptimized', + 'c_std=gnu11', + ], +) + +gio = dependency('gio-2.0', version: '>=2.50.0') +gio_unix = dependency('gio-unix-2.0', version: '>=2.50.0') +glib = dependency('glib-2.0', version: '>=2.50.0') +gobject = dependency('gobject-2.0', version: '>=2.50.0') +gsound = dependency('gsound') +gudev = dependency('gudev-1.0', version: '>=232') +json_glib = dependency('json-glib-1.0') + +prefix = get_option('prefix') + +datadir = join_paths(prefix, get_option('datadir')) +libdir = join_paths(prefix, get_option('libdir')) +girdir = join_paths(prefix, join_paths(datadir, 'gir-1.0')) +ifacedir = join_paths(prefix, join_paths(datadir, 'dbus-1', 'interfaces')) +servicedir = join_paths(prefix, join_paths(datadir, 'dbus-1', 'services')) +libexecdir = join_paths(prefix, get_option('libexecdir')) +typelibdir = join_paths(prefix, join_paths(libdir, 'girepository-1.0')) +vapidir = join_paths(prefix, join_paths(datadir, 'vala', 'vapi')) +feedbackd_theme_dir = join_paths(datadir, 'feedbackd', 'themes') + +run_data = configuration_data() +run_data.set('ABS_BUILDDIR', meson.current_build_dir()) +run_data.set('ABS_SRCDIR', meson.current_source_dir()) +configure_file( + input: 'run.in', + output: 'run', + configuration: run_data) + +gnome = import('gnome') + +cc = meson.get_compiler('c') + +test_c_args = [ + '-Wcast-align', + '-Wdate-time', + ['-Werror=format-security', '-Werror=format=2'], + '-Wendif-labels', + '-Werror=incompatible-pointer-types', + '-Werror=missing-declarations', + '-Werror=overflow', + '-Werror=return-type', + '-Werror=shift-count-overflow', + '-Werror=shift-overflow=2', + '-Werror=implicit-fallthrough=3', + '-Wformat-nonliteral', + '-Wformat-security', + '-Winit-self', + '-Wmaybe-uninitialized', + '-Wmissing-field-initializers', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wnested-externs', + '-Wno-missing-field-initializers', + '-Wno-sign-compare', + '-Wno-strict-aliasing', + '-Wno-unused-parameter', + '-Wold-style-definition', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wstrict-prototypes', + '-Wtype-limits', + '-Wundef', + '-Wunused-function', +] + +if get_option('buildtype') != 'plain' + test_c_args += '-fstack-protector-strong' +endif + +global_c_args = [ + '-DFEEDBACKD_THEME_DIR="@0@"'.format(feedbackd_theme_dir), + '-DLIBFEEDBACK_COMPILATION', +] + +add_project_arguments( + global_c_args + cc.get_supported_arguments(test_c_args), + language: 'c' +) + +introspection_feature = get_option('introspection') +introspection = introspection_feature.enabled() or introspection_feature.auto() + +apiversion = '0.0' +libname = 'libfeedback-' + apiversion + +json_glib_validate = find_program('json-glib-validate', required: false) + +subdir('data') +subdir('libfeedback') +subdir('src') +subdir('cli') +subdir('tests') +subdir('doc') + +summary = [ + '', + '---------------', + 'feedbackd @0@'.format(meson.project_version()), + '', + ' Daemon: @0@'.format(get_option('daemon')), + ' Documentation: @0@'.format(get_option('gtk_doc')), + ' Introspection: @0@'.format(introspection), + ' Vapi: @0@'.format(get_option('vapi')), + ' Tests: @0@'.format(get_option('tests')), + '---------------', + '' +] +message('\n'.join(summary)) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..cbf8b76 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,12 @@ +option('introspection', type: 'feature', value: 'auto') +option('vapi', type: 'boolean', value: true) + +option('daemon', + type: 'boolean', value: true, + description: 'Whether to build the daeon') +option('gtk_doc', + type: 'boolean', value: false, + description: 'Whether to generate the API reference for Handy') +option('tests', + type: 'boolean', value: true, + description: 'Whether to compile unit tests') diff --git a/run.in b/run.in new file mode 100755 index 0000000..21e1e10 --- /dev/null +++ b/run.in @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +ABS_BUILDDIR='@ABS_BUILDDIR@' +ABS_SRCDIR='@ABS_SRCDIR@' +FEEDBACK_THEME=${FEEDBACK_THEME:-"${ABS_SRCDIR}/data/default.json"} + +export FEEDBACK_THEME +export GSETTINGS_SCHEMA_DIR="${ABS_BUILDDIR}/data" + +set -x +exec "${ABS_BUILDDIR}/src/feedbackd" "$@" diff --git a/src/fbd-dev-sound.c b/src/fbd-dev-sound.c new file mode 100644 index 0000000..c4777e5 --- /dev/null +++ b/src/fbd-dev-sound.c @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + * + * See https://www.kernel.org/doc/html/latest/input/ff.html + * and fftest.c from the joystick package. + */ + +#define G_LOG_DOMAIN "fbd-dev-sound" + +#include "fbd-dev-sound.h" +#include "fbd-feedback-sound.h" + +#include + +/** + * SECTION:fbd-dev-sound + * @short_description: Sound interface + * @Title: FbdDevSound + * + * The #FbdDevSound is used to play sounds via the systems audio + * system. + */ + +typedef struct _FbdAsyncData { + FbdDevSoundPlayedCallback callback; + FbdFeedbackSound *feedback; +} FbdAsyncData; + +typedef struct _FbdDevSound { + GObject parent; + + GSoundContext *ctx; +} FbdDevSound; + +static void initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (FbdDevSound, fbd_dev_sound, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)); + +static void +fbd_dev_sound_dispose (GObject *object) +{ + FbdDevSound *self = FBD_DEV_SOUND (object); + + g_clear_object (&self->ctx); + + G_OBJECT_CLASS (fbd_dev_sound_parent_class)->dispose (object); +} + +static gboolean +initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + FbdDevSound *self = FBD_DEV_SOUND (initable); + + self->ctx = gsound_context_new(NULL, error); + if (!self->ctx) + return FALSE; + + return TRUE; +} + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = initable_init; +} + +static void +fbd_dev_sound_class_init (FbdDevSoundClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = fbd_dev_sound_dispose; +} + +static void +fbd_dev_sound_init (FbdDevSound *self) +{ +} + +FbdDevSound * +fbd_dev_sound_new (GError **error) +{ + return FBD_DEV_SOUND (g_initable_new (FBD_TYPE_DEV_SOUND, + NULL, + error, + NULL)); +} + + +static void +on_sound_play_finished_callback (GSoundContext *ctx, + GAsyncResult *res, + FbdAsyncData *data) +{ + GError *err = NULL; + + if (!gsound_context_play_full_finish (ctx, res, &err)) { + g_warning ("Failed to play sound: %s", err->message); + } + + (*data->callback)(data->feedback); + g_object_unref (data->feedback); + g_free (data); +} + + +gboolean +fbd_dev_sound_play (FbdDevSound *self, FbdFeedbackSound *feedback, FbdDevSoundPlayedCallback callback) +{ + FbdAsyncData *data; + + g_return_val_if_fail (FBD_IS_DEV_SOUND (self), FALSE); + g_return_val_if_fail (GSOUND_IS_CONTEXT (self->ctx), FALSE); + + data = g_new0 (FbdAsyncData, 1); + data->callback = callback; + data->feedback = g_object_ref (feedback); + + gsound_context_play_full (self->ctx, NULL, + (GAsyncReadyCallback)on_sound_play_finished_callback, + data, + GSOUND_ATTR_EVENT_ID, fbd_feedback_sound_get_effect (feedback), + GSOUND_ATTR_EVENT_DESCRIPTION, "Feedbackd sound feedback", + NULL); + return TRUE; +} diff --git a/src/fbd-dev-sound.h b/src/fbd-dev-sound.h new file mode 100644 index 0000000..34be5f2 --- /dev/null +++ b/src/fbd-dev-sound.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include + +#include "fbd-feedback-sound.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_DEV_SOUND (fbd_dev_sound_get_type()) + +G_DECLARE_FINAL_TYPE (FbdDevSound, fbd_dev_sound, FBD, DEV_SOUND, GObject); + +typedef void (*FbdDevSoundPlayedCallback)(FbdFeedbackSound *feedback); + +FbdDevSound *fbd_dev_sound_new (GError **error); +gboolean fbd_dev_sound_play (FbdDevSound *self, + FbdFeedbackSound *feedback, + FbdDevSoundPlayedCallback callback); +gboolean fbd_dev_sound_stop (FbdDevSound *self, FbdFeedbackSound *feedback); + +G_END_DECLS diff --git a/src/fbd-dev-vibra.c b/src/fbd-dev-vibra.c new file mode 100644 index 0000000..1f78136 --- /dev/null +++ b/src/fbd-dev-vibra.c @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + * + * See https://www.kernel.org/doc/html/latest/input/ff.html + * and fftest.c from the joystick package. + */ + +#define G_LOG_DOMAIN "fbd-dev-vibra" + +#include "fbd-dev-vibra.h" + +#include + +#include +#include +#include +#include +#include +#include + +/** + * SECTION:fbd-dev-vibra + * @short_description: Haptic motor device interface + * @Title: FbdDevVibra + * + * The #FbdDevVibra is used to interface with haptic motor via the force + * feedback interface. It currently only supports one id at a time. + */ + +enum { + PROP_0, + PROP_DEVICE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef enum { + FBD_DEV_VIBRA_FEATURE_RUMBLE, + FBD_DEV_VIBRA_FEATURE_PERIODIC, + FBD_DEV_VIBRA_FEATURE_GAIN, +} FbdDevVibraFeatureFlags; + +typedef struct _FbdDevVibra { + GObject parent; + + GUdevDevice *device; + gint fd; + gint id; /* currently used id */ + + FbdDevVibraFeatureFlags features; +} FbdDevVibra; + +static void initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (FbdDevVibra, fbd_dev_vibra, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)); + +static void +fbd_dev_vibra_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdDevVibra *self = FBD_DEV_VIBRA (object); + + switch (property_id) { + case PROP_DEVICE: + g_clear_object (&self->device); + self->device = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_dev_vibra_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +#define BITS_PER_LONG (8 * sizeof (long)) + +#define HAS_FEATURE(fbit, a) \ + (a[(fbit/BITS_PER_LONG)] >> ((fbit % BITS_PER_LONG)) & 1) + +static gboolean +initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + FbdDevVibra *self = FBD_DEV_VIBRA (initable); + const char *filename = g_udev_device_get_device_file (self->device); + gulong features[1 + FF_MAX/BITS_PER_LONG]; + struct input_event gain = { 0 }; + + self->fd = open (filename, O_RDWR | O_NONBLOCK, O_RDWR); + if (self->fd < 0) { + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to open '%s': %s", + filename, g_strerror (errno)); + return FALSE; + } + + if (ioctl (self->fd, EVIOCGBIT(EV_FF, sizeof (features)), features) == -1) { + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to probe features of '%s': %s", + filename, g_strerror (errno)); + return FALSE; + } + + if (HAS_FEATURE(FF_RUMBLE, features)) + self->features |= FBD_DEV_VIBRA_FEATURE_RUMBLE; + else { + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "No rumble capable vibra device “%s”: %s", + filename, g_strerror (errno)); + return FALSE; + } + + if (HAS_FEATURE(FF_PERIODIC, features)) + self->features |= FBD_DEV_VIBRA_FEATURE_PERIODIC; + else { + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "No rumble capable vibra device “%s”: %s", + filename, g_strerror (errno)); + return FALSE; + } + + /* Set gain to 75% if supported */ + if (HAS_FEATURE(FF_GAIN, features)) { + self->features |= FBD_DEV_VIBRA_FEATURE_GAIN; + memset(&gain, 0, sizeof(gain)); + gain.type = EV_FF; + gain.code = FF_GAIN; + gain.value = 0xC000; /* [0, 0xFFFF]) */ + + g_debug("Setting master gain to 75%%"); + if (write(self->fd, &gain, sizeof(gain)) != sizeof(gain)) { + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to set gain of '%s': %s", + filename, g_strerror (errno)); + } + } else { + g_debug ("Gain unsupported"); + } + + g_debug ("Vibra device at '%s' usable", filename); + return TRUE; +} + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = initable_init; +} + +static void +fbd_dev_vibra_dispose (GObject *object) +{ + FbdDevVibra *self = FBD_DEV_VIBRA (object); + + g_clear_object (&self->device); + + G_OBJECT_CLASS (fbd_dev_vibra_parent_class)->dispose (object); +} + +static void +fbd_dev_vibra_finalize (GObject *object) +{ + FbdDevVibra *self = FBD_DEV_VIBRA (object); + + if (self->fd >= 0) { + close (self->fd); + self->fd = -1; + } + + G_OBJECT_CLASS (fbd_dev_vibra_parent_class)->finalize (object); +} + +static void +fbd_dev_vibra_class_init (FbdDevVibraClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_dev_vibra_set_property; + object_class->get_property = fbd_dev_vibra_get_property; + + object_class->dispose = fbd_dev_vibra_dispose; + object_class->finalize = fbd_dev_vibra_finalize; + + props[PROP_DEVICE] = + g_param_spec_object ( + "device", + "Device", + "The udev device", + G_UDEV_TYPE_DEVICE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_dev_vibra_init (FbdDevVibra *self) +{ +} + +FbdDevVibra * +fbd_dev_vibra_new (GUdevDevice *device, GError **error) +{ + return FBD_DEV_VIBRA (g_initable_new (FBD_TYPE_DEV_VIBRA, + NULL, + error, + "device", device, + NULL)); +} + +gboolean +fbd_dev_vibra_rumble (FbdDevVibra *self, guint duration, gboolean upload) +{ + struct input_event event = { 0 }; + struct ff_effect effect = { 0 }; + + g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE); + + memset(&effect, 0, sizeof(effect)); + effect.type = FF_RUMBLE; + effect.id = -1; + effect.u.rumble.strong_magnitude = 0x8000; + effect.u.rumble.weak_magnitude = 0; + effect.replay.length = duration; + effect.replay.delay = 0; + + if (upload) { + g_debug("Uploading rumbling vibra effect (%d)", self->fd); + if (ioctl(self->fd, EVIOCSFF, &effect) == -1) { + g_warning ("Failed to upload rumbling vibra effect: %s", g_strerror (errno)); + return FALSE; + } + self->id = effect.id; + } + + g_debug("Playing rumbling vibra effect id %d", effect.id); + event.type = EV_FF; + event.value = 1; + event.code = self->id; + + if (write (self->fd, (const void*) &event, sizeof (event)) < 0) { + g_warning ("Failed to play rumbling vibra effect."); + return FALSE; + } + + return TRUE; +} + +/* TODO: fall back to multiple rumbles when sine not supported */ +gboolean +fbd_dev_vibra_periodic (FbdDevVibra *self, guint duration, guint magnitude, + guint fade_in_level, guint fade_in_time) +{ + struct input_event event; + struct ff_effect effect = { 0 }; + + g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE); + + if (!magnitude) + magnitude = 0x7FFF; + + if (!fade_in_level) + fade_in_level = magnitude; + + if (!fade_in_time) + fade_in_time = duration; + + effect.type = FF_PERIODIC; + effect.id = -1; + effect.u.periodic.waveform = FF_SINE; + effect.u.periodic.period = 10; + effect.u.periodic.magnitude = magnitude; + effect.u.periodic.offset = 0; + effect.u.periodic.phase = 0; + effect.direction = 0x4000; + effect.u.periodic.envelope.attack_length = fade_in_time; + effect.u.periodic.envelope.attack_level = fade_in_level;; + effect.u.periodic.envelope.fade_length = 0; + effect.u.periodic.envelope.fade_level = 0; + effect.trigger.button = 0; + effect.trigger.interval = 0; + effect.replay.length = duration; + effect.replay.delay = 200; + + g_debug("Uploading periodic effect (%d)", self->fd); + if (ioctl(self->fd, EVIOCSFF, &effect) == -1) { + g_warning ("Failed to upload periodic vibra effect: %s", g_strerror (errno)); + return FALSE; + } + + g_debug("Playing periodic vibra effect id %d", effect.id); + event.type = EV_FF; + self->id = event.code = effect.id; + event.value = 1; + + if (write (self->fd, (const void*) &event, sizeof (event)) < 0) { + g_warning ("Failed to play rumbling effect."); + return FALSE; + } + + return TRUE; +} + + +gboolean +fbd_dev_vibra_remove_effect (FbdDevVibra *self) +{ + g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE); + + g_debug("Erasing vibra effect (%d)", self->fd); + if (ioctl(self->fd, EVIOCRMFF, self->id) == -1) { + g_warning ("Failed to erase vibra effect with id %d: %s", self->id, strerror(errno)); + return FALSE; + } + return TRUE; +} + + +gboolean +fbd_dev_vibra_stop(FbdDevVibra *self) +{ + struct input_event stop = { 0 }; + + g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE); + + stop.type = EV_FF; + stop.code = self->id; + stop.value = 0; + + if (write(self->fd, (const void*) &stop, sizeof(stop)) < 0) { + g_warning ("Failed to stop vibra effect with id %d: %s", self->id, strerror(errno)); + return FALSE; + } + + return fbd_dev_vibra_remove_effect (self); +} + +GUdevDevice * +fbd_dev_vibra_get_device(FbdDevVibra *self) +{ + g_return_val_if_fail (FBD_IS_DEV_VIBRA (self), FALSE); + + return self->device; +} diff --git a/src/fbd-dev-vibra.h b/src/fbd-dev-vibra.h new file mode 100644 index 0000000..55c03c6 --- /dev/null +++ b/src/fbd-dev-vibra.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FBD_TYPE_DEV_VIBRA (fbd_dev_vibra_get_type()) + +G_DECLARE_FINAL_TYPE (FbdDevVibra, fbd_dev_vibra, FBD, DEV_VIBRA, GObject); + +FbdDevVibra *fbd_dev_vibra_new (GUdevDevice *device, GError **error); +gboolean fbd_dev_vibra_rumble (FbdDevVibra *device, guint duration, gboolean upload); +gboolean fbd_dev_vibra_periodic (FbdDevVibra *self, guint duration, guint magnitude, + guint fade_in_level, guint fade_in_time); +gboolean fbd_dev_vibra_stop (FbdDevVibra *self); +gboolean fbd_dev_vibra_remove_effect (FbdDevVibra *self); +GUdevDevice *fbd_dev_vibra_get_device(FbdDevVibra *self); + + +G_END_DECLS diff --git a/src/fbd-event.c b/src/fbd-event.c new file mode 100644 index 0000000..3c37a04 --- /dev/null +++ b/src/fbd-event.c @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-event" + +#include "lfb-names.h" +#include "fbd.h" +#include "fbd-enums.h" +#include "fbd-event.h" + +enum { + SIGNAL_FEEDBACKS_ENDED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +enum { + PROP_0, + PROP_ID, + PROP_APP_ID, + PROP_EVENT, + PROP_END_REASON, + PROP_FEEDBACKS_ENDED, + PROP_TIMEOUT, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdEvent { + GObject parent; + + guint id; + char *app_id; + char *event; + + gint timeout; + gboolean expired; + guint timeout_id; + + gboolean ended; + FbdEventEndReason end_reason; + + GSList *feedbacks; +} FbdEvent; + +G_DEFINE_TYPE (FbdEvent, fbd_event, G_TYPE_OBJECT); + +static gboolean +on_timeout_expired (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), G_SOURCE_REMOVE); + + self->expired = TRUE; + self->timeout_id = 0; + return G_SOURCE_REMOVE; +} + +static gboolean +check_ended (FbdEvent *self) +{ + if (!fbd_event_get_feedbacks_ended (self)) + return FALSE; + + self->ended = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FEEDBACKS_ENDED]); + g_signal_emit (self, signals[SIGNAL_FEEDBACKS_ENDED], 0); + return TRUE; +} + +static void +on_fb_ended (FbdEvent *self, FbdFeedbackBase *fb) +{ + switch (self->timeout) { + case FBD_EVENT_TIMEOUT_ONESHOT: + check_ended (self); + break; + case FBD_EVENT_TIMEOUT_LOOP: + if (self->end_reason != FBD_EVENT_END_REASON_NATURAL) + check_ended (self); + else + fbd_feedback_run (fb); + break; + default: + if (!self->expired && self->end_reason == FBD_EVENT_END_REASON_NATURAL) + fbd_feedback_run (fb); + else + check_ended (self); + break; + } +} + +static void +fbd_event_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdEvent *self = FBD_EVENT (object); + + switch (property_id) { + case PROP_ID: + self->id = g_value_get_int (value); + break; + case PROP_APP_ID: + g_free (self->app_id); + self->app_id = g_value_dup_string (value); + break; + case PROP_EVENT: + g_free (self->event); + self->event = g_value_dup_string (value); + break; + case PROP_TIMEOUT: + self->timeout = g_value_get_int (value); + break; + case PROP_END_REASON: + fbd_event_set_end_reason (self, g_value_get_enum (value)); + break; + case PROP_FEEDBACKS_ENDED: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_event_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdEvent *self = FBD_EVENT (object); + + switch (property_id) { + case PROP_ID: + g_value_set_int (value, self->id); + break; + case PROP_APP_ID: + g_value_set_string (value, self->app_id); + break; + case PROP_EVENT: + g_value_set_string (value, self->event); + break; + case PROP_TIMEOUT: + g_value_set_int (value, self->timeout); + break; + case PROP_END_REASON: + g_value_set_enum (value, fbd_event_get_end_reason (self)); + break; + case PROP_FEEDBACKS_ENDED: + g_value_set_boolean (value, self->ended); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_event_dispose (GObject *object) +{ + FbdEvent *self = FBD_EVENT (object); + + g_clear_handle_id (&self->timeout_id, g_source_remove); + + if (self->feedbacks) { + /* Feedbacks end themselves when unrefed */ + g_slist_free_full (self->feedbacks, g_object_unref); + self->feedbacks = NULL; + } + + G_OBJECT_CLASS (fbd_event_parent_class)->dispose (object); +} + +static void +fbd_event_finalize (GObject *object) +{ + FbdEvent *self = FBD_EVENT (object); + + g_clear_pointer (&self->app_id, g_free); + g_clear_pointer (&self->event, g_free); + + G_OBJECT_CLASS (fbd_event_parent_class)->finalize (object); +} + +static void +fbd_event_class_init (FbdEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_event_set_property; + object_class->get_property = fbd_event_get_property; + + object_class->dispose = fbd_event_dispose; + object_class->finalize = fbd_event_finalize; + + props[PROP_ID] = + g_param_spec_int ( + "id", + "Id", + "The event id", + 0, G_MAXINT, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_APP_ID] = + g_param_spec_string ( + "app-id", + "App id", + "The application id", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_EVENT] = + g_param_spec_string ( + "event", + "Event", + "The event name", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_TIMEOUT] = + g_param_spec_int ( + "timeout", + "Timeout", + "Timeout after which feedback for event should end", + -1, G_MAXINT, -1, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_FEEDBACKS_ENDED] = + g_param_spec_boolean ( + "feedbacks-ended", + "Feedbacks ended", + "Whether all feedbacks have ended playing", + FALSE, + G_PARAM_READWRITE | 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 ends/ended", + FBD_TYPE_EVENT_END_REASON, + FBD_EVENT_END_REASON_NATURAL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * FbdEvent::feedbacks-ended: + * + * Emitted when all feedbacks associated with this event ended. + */ + signals[SIGNAL_FEEDBACKS_ENDED] = g_signal_new ("feedbacks-ended", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); +} + +static void +fbd_event_init (FbdEvent *self) +{ + self->timeout = -1; +} + +FbdEvent * +fbd_event_new (gint id, const gchar *app_id, const gchar *event, gint timeout) +{ + return FBD_EVENT (g_object_new (FBD_TYPE_EVENT, + "id", id, + "app_id", app_id, + "event", event, + "timeout", timeout, + NULL)); +} + +const gchar * +fbd_event_get_event (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), NULL); + + return self->event; +} + +const gchar * +fbd_event_get_app_id (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), NULL); + + return self->app_id; +} + +guint +fbd_event_get_id (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), 0); + + return self->id; +} + +gint +fbd_event_get_timeout (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), -1); + + return self->timeout; +} + +/** + * fbd_event_add_feedback: + * @self: The event that gets a feedback added + * @feedback: (transfer-none): The feedback to add + * + * Add a feedback to the list of feedbacks triggered by event + */ +void +fbd_event_add_feedback (FbdEvent *self, FbdFeedbackBase *feedback) +{ + self->feedbacks = g_slist_prepend (self->feedbacks, g_object_ref(feedback)); + g_object_set_data (G_OBJECT (feedback), "event-id", GUINT_TO_POINTER(self->id)); + g_signal_connect_object (feedback, + "ended", + (GCallback) on_fb_ended, + self, + G_CONNECT_SWAPPED); +} + +GSList * +fbd_event_get_feedbacks (FbdEvent *self) +{ + return self->feedbacks; +} + +gint +fbd_event_remove_feedback (FbdEvent *self, FbdFeedbackBase *feedback) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), 0); + + self->feedbacks = g_slist_remove (self->feedbacks, feedback); + return g_slist_length (self->feedbacks); +} + +/** + * fbd_event_run_feedbacks: + * @self: The Event + * + * Run all feedbacks associated for an event. + */ +void +fbd_event_run_feedbacks (FbdEvent *self) +{ + GSList *l; + + g_return_if_fail (FBD_IS_EVENT (self)); + + g_debug ("Running %d feedbacks for event %d", g_slist_length (self->feedbacks), self->id); + + if (!self->feedbacks) + return; + + if (self->timeout > 0) { + self->timeout_id = g_timeout_add_seconds (self->timeout, + (GSourceFunc)on_timeout_expired, + self); + g_source_set_name_by_id (self->timeout_id, "event timeout source"); + } + + for (l = self->feedbacks; l; l = l->next) { + FbdFeedbackBase *fb = FBD_FEEDBACK_BASE (l->data); + fbd_feedback_run (fb); + } +} + +/** + * fbd_event_end_feedbacks: + * @self: The Event + * + * End all running feedbacks as early as possible. + */ +void +fbd_event_end_feedbacks (FbdEvent *self) +{ + g_return_if_fail (FBD_IS_EVENT (self)); + + fbd_event_set_end_reason (self, FBD_EVENT_END_REASON_EXPLICIT); + g_debug ("Ending %d feedbacks for event %d", g_slist_length (self->feedbacks), self->id); + g_slist_foreach (self->feedbacks, (GFunc)fbd_feedback_end, NULL); +} + +/** + * fbd_event_get_feedbacks_ended: + * @self: The Event + * + * Whether all feedbacks have finished running. + * + * Returns: %TRUE if all feedbacks have finished, otherwise %FALSE. + */ +gboolean +fbd_event_get_feedbacks_ended (FbdEvent *self) +{ + GSList *l; + + g_return_val_if_fail (FBD_IS_EVENT (self), FALSE); + + if (!self->feedbacks) + return TRUE; + + for (l = self->feedbacks; l ; l = l->next) { + if (!fbd_feedback_get_ended (FBD_FEEDBACK_BASE (l->data))) + return FALSE; + } + + return TRUE; +} + +/** + * fbd_event_set_end_reason: + * @self: The Event + * @reason: The reason why feedback for the event ended + * + * Sets the reason why feedback for the event ends/has ended. + */ +void +fbd_event_set_end_reason (FbdEvent *self, FbdEventEndReason reason) +{ + g_return_if_fail (FBD_IS_EVENT (self)); + + if (self->end_reason == reason) + return; + self->end_reason = reason; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_REASON]); +} + +/** + * fbd_event_get_end_reason: + * @self: The Event + * + * Returns: The reason why feedback for the event ended. + */ +FbdEventEndReason +fbd_event_get_end_reason (FbdEvent *self) +{ + g_return_val_if_fail (FBD_IS_EVENT (self), FBD_EVENT_END_REASON_NATURAL); + + return self->end_reason; +} diff --git a/src/fbd-event.h b/src/fbd-event.h new file mode 100644 index 0000000..38599dc --- /dev/null +++ b/src/fbd-event.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-base.h" +#include "fbd-feedback-manager.h" + +#include + +G_BEGIN_DECLS + +typedef enum _FbdEventEndReason { + /* No feedback in current theme for this event */ + FBD_EVENT_END_REASON_NOT_FOUND = -1, + /* all feedbacks finished playing their natural length */ + FBD_EVENT_END_REASON_NATURAL = 0, + /* The timer expired */ + FBD_EVENT_END_REASON_EXPIRED = 1, + /* Application wanted to end feedbacks explicitly */ + FBD_EVENT_END_REASON_EXPLICIT = 2, +} FbdEventEndReason; + +typedef enum _FbdEventTimeout { + /* Run each feedback once */ + FBD_EVENT_TIMEOUT_ONESHOT = -1, + FBD_EVENT_TIMEOUT_LOOP = 0, +} FbdEventTimeout; + +#define FBD_TYPE_EVENT (fbd_event_get_type()) + +G_DECLARE_FINAL_TYPE (FbdEvent, fbd_event, FBD, EVENT, GObject); + +FbdEvent *fbd_event_new (gint id, const gchar *app_id, const gchar *event, gint timeout); +const gchar *fbd_event_get_event (FbdEvent *event); +const gchar *fbd_event_get_app_id (FbdEvent *event); +guint fbd_event_get_id (FbdEvent *event); +gint fbd_event_get_timeout (FbdEvent *self); +void fbd_event_set_end_reason (FbdEvent *self, FbdEventEndReason reason); +FbdEventEndReason fbd_event_get_end_reason (FbdEvent *self); +GSList * fbd_event_get_feedbacks (FbdEvent *self); +void fbd_event_add_feedback (FbdEvent *self, + FbdFeedbackBase *feedback); +gint fbd_event_remove_feedback (FbdEvent *self, + FbdFeedbackBase *feedback); +void fbd_event_run_feedbacks (FbdEvent *self); +void fbd_event_end_feedbacks (FbdEvent *self); +gboolean fbd_event_get_feedbacks_ended (FbdEvent *self); + +G_END_DECLS diff --git a/src/fbd-feedback-base.c b/src/fbd-feedback-base.c new file mode 100644 index 0000000..d9b91ec --- /dev/null +++ b/src/fbd-feedback-base.c @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-base" + +#include "fbd-feedback-base.h" + +/** + * SECTION:fbd-feedback-base + * @short_description: Base class for different feedback types + * @Title: FbdFeedbackManager + * + * You usually don't want to create objects of this type. It just + * serves as a base class for other feedback types. + */ + +enum { + PROP_0, + PROP_EVENT_NAME, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + SIGNAL_ENDED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +typedef struct _FbdFeedbackBasePrivate { + gchar *event_name; + gboolean ended; +} FbdFeedbackBasePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (FbdFeedbackBase, fbd_feedback_base, G_TYPE_OBJECT); + +static void +fbd_feedback_base_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackBase *self = FBD_FEEDBACK_BASE (object); + FbdFeedbackBasePrivate *priv = fbd_feedback_base_get_instance_private (self); + + switch (property_id) { + case PROP_EVENT_NAME: + g_free (priv->event_name); + priv->event_name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_feedback_base_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackBase *self = FBD_FEEDBACK_BASE (object); + FbdFeedbackBasePrivate *priv = fbd_feedback_base_get_instance_private (self); + + switch (property_id) { + case PROP_EVENT_NAME: + g_value_set_string (value, priv->event_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_base_dispose (GObject *object) +{ + FbdFeedbackBase *self = FBD_FEEDBACK_BASE (object); + + /* end feedback if running */ + if (!fbd_feedback_get_ended (self)) + fbd_feedback_end (self); + + G_OBJECT_CLASS (fbd_feedback_base_parent_class)->dispose (object); +} + +static void +fbd_feedback_base_finalize (GObject *object) +{ + FbdFeedbackBase *self = FBD_FEEDBACK_BASE (object); + FbdFeedbackBasePrivate *priv = fbd_feedback_base_get_instance_private (self); + + g_clear_pointer (&priv->event_name, g_free); + + G_OBJECT_CLASS (fbd_feedback_base_parent_class)->finalize (object); +} + +static void +fbd_feedback_base_class_init (FbdFeedbackBaseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_feedback_base_set_property; + object_class->get_property = fbd_feedback_base_get_property; + + object_class->dispose = fbd_feedback_base_dispose; + object_class->finalize = fbd_feedback_base_finalize; + + props[PROP_EVENT_NAME] = + g_param_spec_string ( + "event-name", + "Event Name", + "The event this feedback is associated with", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * FbdFeedbackBase::ended: + * + * Emitted when the feedback has ended + */ + signals[SIGNAL_ENDED] = g_signal_new ("ended", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); +} + +static void +fbd_feedback_base_init (FbdFeedbackBase *self) +{ +} + +/** + * fbd_feedback_get_event_name: + * @self: The feedback + * + * Returns: the name from the event naming spec this feedback is associated with. + */ +const gchar * +fbd_feedback_get_event_name (FbdFeedbackBase *self) +{ + FbdFeedbackBasePrivate *priv; + + g_return_val_if_fail (FBD_IS_FEEDBACK_BASE (self), NULL); + priv = fbd_feedback_base_get_instance_private (self); + + return priv->event_name; +} + +/** + * fbd_feedback_run: + * @self: The feedback to run + * + * Emit the feedback. + */ +void +fbd_feedback_run (FbdFeedbackBase *self) +{ + FbdFeedbackBaseClass *klass; + FbdFeedbackBasePrivate *priv; + + g_return_if_fail (FBD_IS_FEEDBACK_BASE (self)); + priv = fbd_feedback_base_get_instance_private (self); + + priv->ended = FALSE; + klass = FBD_FEEDBACK_BASE_GET_CLASS (self); + g_return_if_fail (klass->run); + klass->run (self); +} + +/** + * fbd_feedback_end: + * @self: The feedback to end + * + * End the feedback immediately. + */ +void +fbd_feedback_end (FbdFeedbackBase *self) +{ + FbdFeedbackBaseClass *klass; + + g_return_if_fail (FBD_IS_FEEDBACK_BASE (self)); + + klass = FBD_FEEDBACK_BASE_GET_CLASS (self); + g_return_if_fail (klass->end); + klass->end (self); +} + + +/** + * fbd_feedback_get_ended: + * @self: The feedback + * + * Whether the feedback is ended. + * + * Returns: %TRUE if feedback has ended, otherwise %FALSE. + */ +gboolean +fbd_feedback_get_ended (FbdFeedbackBase *self) +{ + FbdFeedbackBasePrivate *priv = fbd_feedback_base_get_instance_private (self); + + return priv->ended; +} + +/** + * fbd_feedback_base_done: + * @self: The feedback + * + * Invoked by a derived classes to notify that it's done emitting feedback, + * e.g. when the vibra motor stopped or a sound finished playing. + */ +void +fbd_feedback_base_done (FbdFeedbackBase *self) +{ + FbdFeedbackBasePrivate *priv = fbd_feedback_base_get_instance_private (self); + + priv->ended = TRUE; + g_signal_emit (self, signals[SIGNAL_ENDED], 0); +} diff --git a/src/fbd-feedback-base.h b/src/fbd-feedback-base.h new file mode 100644 index 0000000..893a884 --- /dev/null +++ b/src/fbd-feedback-base.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_BASE (fbd_feedback_base_get_type()) + +G_DECLARE_DERIVABLE_TYPE (FbdFeedbackBase, fbd_feedback_base, FBD, FEEDBACK_BASE, GObject); + +struct _FbdFeedbackBaseClass +{ + GObjectClass parent_class; + + void (*run) (FbdFeedbackBase *self); + void (*end) (FbdFeedbackBase *self); +}; + + +const gchar *fbd_feedback_get_event_name (FbdFeedbackBase *self); +void fbd_feedback_run (FbdFeedbackBase *self); +void fbd_feedback_end (FbdFeedbackBase *self); +gboolean fbd_feedback_get_ended (FbdFeedbackBase *self); +void fbd_feedback_base_done (FbdFeedbackBase *self); + +G_END_DECLS diff --git a/src/fbd-feedback-dummy.c b/src/fbd-feedback-dummy.c new file mode 100644 index 0000000..7d405e7 --- /dev/null +++ b/src/fbd-feedback-dummy.c @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-dummy" + +#include "fbd-enums.h" +#include "fbd-feedback-dummy.h" +#include "fbd-feedback-manager.h" + +/** + * SECTION:fbd-feedback-dummy + * @short_description: Describes a dummy feedback event that does nothing + * @Title: FbdFeedbackDummy + */ + +enum { + PROP_0, + PROP_DURATION, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackDummy { + FbdFeedbackBase parent; + + guint duration; + guint timer_id; +} FbdFeedbackDummy; + +G_DEFINE_TYPE (FbdFeedbackDummy, fbd_feedback_dummy, FBD_TYPE_FEEDBACK_BASE); + +static void +fbd_feedback_dummy_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackDummy *self = FBD_FEEDBACK_DUMMY (object); + + switch (property_id) { + case PROP_DURATION: + self->duration = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_dummy_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackDummy *self = FBD_FEEDBACK_DUMMY (object); + + switch (property_id) { + case PROP_DURATION: + g_value_set_uint (value, self->duration); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +on_timeout_expired (FbdFeedbackDummy *self) +{ + fbd_feedback_base_done (FBD_FEEDBACK_BASE(self)); + return G_SOURCE_REMOVE; +} + +static void +fbd_feedback_dummy_run (FbdFeedbackBase *base) +{ + FbdFeedbackDummy *self = FBD_FEEDBACK_DUMMY (base); + + if (self->duration) { + self->timer_id = g_timeout_add (self->duration, + (GSourceFunc)on_timeout_expired, + self); + g_source_set_name_by_id (self->timer_id, "feedback-dummy-timer"); + } else { + fbd_feedback_base_done (base); + } +} + +static void +fbd_feedback_dummy_end (FbdFeedbackBase *base) +{ + FbdFeedbackDummy *self = FBD_FEEDBACK_DUMMY (base); + + g_clear_handle_id(&self->timer_id, g_source_remove); + fbd_feedback_base_done (base); +} + +static void +fbd_feedback_dummy_class_init (FbdFeedbackDummyClass *klass) +{ + FbdFeedbackBaseClass *base_class = FBD_FEEDBACK_BASE_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_feedback_dummy_set_property; + object_class->get_property = fbd_feedback_dummy_get_property; + + base_class->run = fbd_feedback_dummy_run; + base_class->end = fbd_feedback_dummy_end; + + props[PROP_DURATION] = + g_param_spec_uint ( + "duration", + "Duration", + "Dummy event duration in msecs", + 0, G_MAXUINT, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_dummy_init (FbdFeedbackDummy *self) +{ +} diff --git a/src/fbd-feedback-dummy.h b/src/fbd-feedback-dummy.h new file mode 100644 index 0000000..2df8328 --- /dev/null +++ b/src/fbd-feedback-dummy.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-base.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_DUMMY (fbd_feedback_dummy_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackDummy, fbd_feedback_dummy, FBD, FEEDBACK_DUMMY, FbdFeedbackBase); + +G_END_DECLS diff --git a/src/fbd-feedback-manager.c b/src/fbd-feedback-manager.c new file mode 100644 index 0000000..b01dd8b --- /dev/null +++ b/src/fbd-feedback-manager.c @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-manager" + +#include "lfb-names.h" +#include "fbd.h" +#include "fbd-dev-vibra.h" +#include "fbd-event.h" +#include "fbd-feedback-vibra.h" +#include "fbd-feedback-manager.h" +#include "fbd-feedback-theme.h" + +#include +#include +#include + +#define FEEDBACKD_SCHEMA_ID "org.sigxcpu.feedbackd" +#define FEEDBACKD_KEY_PROFILE "profile" + +/** + * 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; + + GSettings *settings; + FbdFeedbackProfileLevel level; + FbdFeedbackTheme *theme; + guint next_id; + + GHashTable *events; + + /* Hardware interaction */ + GUdevClient *client; + FbdDevVibra *vibra; + FbdDevSound *sound; +} 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 remove", g_udev_device_get_sysfs_path (dev)); + g_clear_object (&self->vibra); + } + } else if (g_strcmp0 (action, "add") == 0 || !self->vibra) { + 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)); + self->vibra = fbd_dev_vibra_new (device, &err); + if (!self->vibra) { + g_warning ("Failed to init vibra device: %s", err->message); + g_clear_error (&err); + } + } + } +} + +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"); + + 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); + + /* 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); +} + +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, *l; + gint event_id; + + g_debug ("Event '%s' for '%s'", arg_event, arg_app_id); + + 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 (arg_timeout < -1) + arg_timeout = -1; + + event_id = self->next_id++; + + event = fbd_event_new (event_id, arg_app_id, arg_event, arg_timeout); + g_hash_table_insert (self->events, GUINT_TO_POINTER (event_id), event); + + feedbacks = fbd_feedback_theme_lookup_feedback (self->theme, self->level, event); + + if (feedbacks) { + for (l = feedbacks; l; l = l->next) { + FbdFeedbackBase *fb = l->data; + fbd_event_add_feedback (event, fb); + g_object_unref (fb); + } + g_slist_free (feedbacks); + + g_signal_connect_object (event, "feedbacks-ended", + (GCallback) on_event_feedbacks_ended, + self, + G_CONNECT_SWAPPED); + + fbd_event_run_feedbacks (event); + } else { + /* No feedbacks found, so we're done */ + lfb_gdbus_feedback_emit_feedback_ended + (LFB_GDBUS_FEEDBACK(self), event_id, + FBD_EVENT_END_REASON_NOT_FOUND); + } + + lfb_gdbus_feedback_complete_trigger_feedback (object, invocation, event_id); + 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) +{ + g_autoptr (GError) err = NULL; + FbdFeedbackManager *self = FBD_FEEDBACK_MANAGER (object); + const gchar *themefile; + + G_OBJECT_CLASS (fbd_feedback_manager_parent_class)->constructed (object); + + themefile = g_getenv ("FEEDBACK_THEME"); + if (!themefile) + themefile = FEEDBACKD_THEME_DIR "/default.json"; + + self->theme = fbd_feedback_theme_new_from_file (themefile, &err); + if (!self->theme) { + /* No point to carry on */ + g_error ("Failed to load default theme: %s", err->message); + } + + g_signal_connect (self, "notify::profile", (GCallback)on_profile_changed, NULL); + lfb_gdbus_feedback_set_profile (LFB_GDBUS_FEEDBACK (self), + fbd_feedback_profile_level_to_string(self->level)); + + 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); + 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", + 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); +} + +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; +} + +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; +} diff --git a/src/fbd-feedback-manager.h b/src/fbd-feedback-manager.h new file mode 100644 index 0000000..964dcc5 --- /dev/null +++ b/src/fbd-feedback-manager.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-dev-vibra.h" +#include "fbd-dev-sound.h" +#include "lfb-gdbus.h" +#include + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_MANAGER (fbd_feedback_manager_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackManager, fbd_feedback_manager, FBD, FEEDBACK_MANAGER, LfbGdbusFeedbackSkeleton); + +FbdFeedbackManager *fbd_feedback_manager_get_default (void); +FbdDevVibra *fbd_feedback_manager_get_dev_vibra (FbdFeedbackManager *self); +FbdDevSound *fbd_feedback_manager_get_dev_sound (FbdFeedbackManager *self); +gboolean fbd_feedback_manager_set_profile (FbdFeedbackManager *self, const gchar *profile); + +G_END_DECLS diff --git a/src/fbd-feedback-profile.c b/src/fbd-feedback-profile.c new file mode 100644 index 0000000..21a53fa --- /dev/null +++ b/src/fbd-feedback-profile.c @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-profile" + +#include "fbd-feedback-dummy.h" +#include "fbd-feedback-profile.h" +#include "fbd-feedback-sound.h" +#include "fbd-feedback-vibra-periodic.h" +#include "fbd-feedback-vibra-rumble.h" + +#include + +#define FBD_FEEDBACK_CLS_PREFIX "FbdFeedback" + +enum { + PROP_0, + PROP_NAME, + PROP_FEEDBACKS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +/** + * SECTION:fbd-feedback-profile + * @short_description: A single profile in a #FbdFeedback + * @Title: FbdFeedbackProfile + */ + +typedef struct _FbdFeedbackProfile { + GObject parent; + + gchar *name; + GHashTable *feedbacks; +} FbdFeedbackProfile; + +static void json_serializable_iface_init (JsonSerializableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (FbdFeedbackProfile, fbd_feedback_profile, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (JSON_TYPE_SERIALIZABLE, + json_serializable_iface_init)); + + +static JsonNode * +fbd_feedback_profile_serializable_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (serializable); + JsonNode *node = NULL; + + if (g_strcmp0 (property_name, "feedbacks") == 0) { + GHashTableIter iter; + gpointer key; + FbdFeedbackProfile *profile; + JsonArray *array = json_array_sized_new (FBD_FEEDBACK_PROFILE_N_PROFILES); + + g_hash_table_iter_init (&iter, self->feedbacks); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *) &profile)) { + json_array_add_element (array, json_gobject_serialize (G_OBJECT(profile))); + } + node = json_node_init_array (json_node_alloc (), array); + } else { + node = json_serializable_default_serialize_property (serializable, + property_name, + value, + pspec); + } + return node; +} + +static GType +feedback_get_type (JsonNode *feedback_node) +{ + g_autofree gchar *type_name = NULL; + JsonObject *obj = json_node_get_object (feedback_node); + JsonNode *type_node = json_object_get_member (obj, "type"); + GType gtype = 0; + + /* Ensure all feedback types so the json parsing can use them */ + g_type_ensure (FBD_TYPE_FEEDBACK_DUMMY); + g_type_ensure (FBD_TYPE_FEEDBACK_VIBRA_PERIODIC); + g_type_ensure (FBD_TYPE_FEEDBACK_VIBRA_RUMBLE); + g_type_ensure (FBD_TYPE_FEEDBACK_SOUND); + + g_return_val_if_fail (type_node, FBD_TYPE_FEEDBACK_DUMMY); + + type_name = g_strdup_printf ("FbdFeedback%s", + json_node_get_string (type_node)); + type_name[strlen(FBD_FEEDBACK_CLS_PREFIX)] = + g_ascii_toupper (type_name[strlen(FBD_FEEDBACK_CLS_PREFIX)]); + gtype = g_type_from_name (type_name); + + g_debug ("Feedback %s, type %lu", type_name, gtype); + g_return_val_if_fail (gtype, FBD_TYPE_FEEDBACK_DUMMY); + return gtype; +} + +static gboolean +fbd_feedback_profile_serializable_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node) +{ + if (g_strcmp0 (property_name, "feedbacks") == 0) { + if (JSON_NODE_TYPE (property_node) == JSON_NODE_NULL) { + g_value_set_pointer (value, NULL); + return TRUE; + } else if (JSON_NODE_TYPE (property_node) == JSON_NODE_ARRAY) { + JsonArray *array = json_node_get_array (property_node); + guint i, array_len = json_array_get_length (array); + GHashTable *feedbacks = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_object_unref); + for (i = 0; i < array_len; i++) { + JsonNode *element_node = json_array_get_element (array, i); + + if (JSON_NODE_HOLDS_OBJECT (element_node)) { + gchar *event_name; + FbdFeedbackBase *feedback; + GType gtype = feedback_get_type (element_node); + + feedback = FBD_FEEDBACK_BASE (json_gobject_deserialize (gtype, element_node)); + event_name = g_strdup (fbd_feedback_get_event_name (FBD_FEEDBACK_BASE(feedback))); + g_hash_table_insert (feedbacks, event_name, feedback); + } else { + return FALSE; + } + } + g_value_set_boxed (value, feedbacks); + return TRUE; + } + return FALSE; + } else { + return json_serializable_default_deserialize_property (serializable, + property_name, + value, + pspec, + property_node); + } + return FALSE; +} + +static void +fbd_feedback_profile_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (object); + + switch (property_id) { + case PROP_NAME: + g_free (self->name); + self->name = g_value_dup_string (value); + break; + case PROP_FEEDBACKS: + if (self->feedbacks) + g_hash_table_unref (self->feedbacks); + self->feedbacks = g_value_get_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_feedback_profile_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (object); + + switch (property_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_FEEDBACKS: + g_value_set_boxed (value, self->feedbacks); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_profile_dispose (GObject *object) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (object); + + g_clear_pointer (&self->feedbacks, g_hash_table_unref); + + G_OBJECT_CLASS (fbd_feedback_profile_parent_class)->dispose (object); +} + +static void +fbd_feedback_profile_finalize (GObject *object) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (object); + + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (fbd_feedback_profile_parent_class)->finalize (object); +} + +static void +json_serializable_iface_init (JsonSerializableIface *iface) +{ + iface->serialize_property = fbd_feedback_profile_serializable_serialize_property; + iface->deserialize_property = fbd_feedback_profile_serializable_deserialize_property; +} + +static void +fbd_feedback_profile_constructed (GObject *object) +{ + FbdFeedbackProfile *self = FBD_FEEDBACK_PROFILE (object); + + G_OBJECT_CLASS (fbd_feedback_profile_parent_class)->constructed (object); + + if (!self->feedbacks) { + self->feedbacks = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_object_unref); + } +} + + +static void +fbd_feedback_profile_class_init (FbdFeedbackProfileClass *klass) +{ + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_feedback_profile_set_property; + object_class->get_property = fbd_feedback_profile_get_property; + + object_class->constructed = fbd_feedback_profile_constructed; + object_class->dispose = fbd_feedback_profile_dispose; + object_class->finalize = fbd_feedback_profile_finalize; + + props[PROP_NAME] = + g_param_spec_string ( + "name", + "Name", + "The feedback name", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_FEEDBACKS] = + g_param_spec_boxed ( + "feedbacks", + "Feedbacks", + "The feedbacks for this profile", + G_TYPE_HASH_TABLE, + /* Can't be CONSTRUCT_ONLY since json-glib can't handle it */ + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_profile_init (FbdFeedbackProfile *self) +{ +} + +FbdFeedbackProfile * +fbd_feedback_profile_new (const gchar *name) +{ + return g_object_new(FBD_TYPE_FEEDBACK_PROFILE, + "name", name, + NULL); +} + +const char * +fbd_feedback_profile_get_name (FbdFeedbackProfile *self) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_PROFILE (self), ""); + + return self->name; +} + +void +fbd_feedback_profile_add_feedback (FbdFeedbackProfile *self, FbdFeedbackBase *feedback) +{ + g_return_if_fail (FBD_IS_FEEDBACK_PROFILE (self)); + gchar *name = g_strdup (fbd_feedback_get_event_name (feedback)); + + /* TODO: allow for more than one feedback per event and profile */ + g_hash_table_insert (self->feedbacks, name, g_object_ref (feedback)); +} + +FbdFeedbackBase * +fbd_feedback_profile_get_feedback (FbdFeedbackProfile *self, const char *event_name) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_PROFILE (self), NULL); + + return g_hash_table_lookup (self->feedbacks, event_name); +} + +FbdFeedbackProfileLevel +fbd_feedback_profile_level (const char *name) +{ + FbdFeedbackProfileLevel profile = FBD_FEEDBACK_PROFILE_LEVEL_UNKNOWN; + + if (!g_strcmp0("silent", name)) { + profile = FBD_FEEDBACK_PROFILE_LEVEL_SILENT; + } else if (!g_strcmp0("quiet", name)) { + profile = FBD_FEEDBACK_PROFILE_LEVEL_QUIET; + } else if (!g_strcmp0("full", name)) { + profile = FBD_FEEDBACK_PROFILE_LEVEL_FULL; + } + + return profile; +} + +const char* +fbd_feedback_profile_level_to_string (FbdFeedbackProfileLevel level) +{ + switch (level) { + case FBD_FEEDBACK_PROFILE_LEVEL_SILENT: + return "silent"; + case FBD_FEEDBACK_PROFILE_LEVEL_QUIET: + return "quiet"; + case FBD_FEEDBACK_PROFILE_LEVEL_FULL: + return "full"; + case FBD_FEEDBACK_PROFILE_LEVEL_UNKNOWN: + default: + return "full"; + } +} diff --git a/src/fbd-feedback-profile.h b/src/fbd-feedback-profile.h new file mode 100644 index 0000000..9844172 --- /dev/null +++ b/src/fbd-feedback-profile.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include + +#include + +G_BEGIN_DECLS + +typedef enum _FbdFeedbackProfileLevel { + FBD_FEEDBACK_PROFILE_LEVEL_UNKNOWN = -1, + FBD_FEEDBACK_PROFILE_LEVEL_SILENT = 0, + FBD_FEEDBACK_PROFILE_LEVEL_QUIET, + FBD_FEEDBACK_PROFILE_LEVEL_FULL, + FBD_FEEDBACK_PROFILE_N_PROFILES, +} FbdFeedbackProfileLevel; + +#define FBD_TYPE_FEEDBACK_PROFILE (fbd_feedback_profile_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackProfile, fbd_feedback_profile, FBD, FEEDBACK_PROFILE, GObject); + +FbdFeedbackProfile *fbd_feedback_profile_new (const gchar *name); +const gchar *fbd_feedback_profile_get_name (FbdFeedbackProfile *self); +void fbd_feedback_profile_add_feedback (FbdFeedbackProfile *self, + FbdFeedbackBase *feedback); +FbdFeedbackBase *fbd_feedback_profile_get_feedback (FbdFeedbackProfile *self, + const char *event_name); +FbdFeedbackProfileLevel fbd_feedback_profile_level (const char *name); +const char* fbd_feedback_profile_level_to_string (FbdFeedbackProfileLevel level); + +G_END_DECLS diff --git a/src/fbd-feedback-sound.c b/src/fbd-feedback-sound.c new file mode 100644 index 0000000..246e8d3 --- /dev/null +++ b/src/fbd-feedback-sound.c @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-sound" + +#include "fbd-enums.h" +#include "fbd-feedback-sound.h" +#include "fbd-feedback-manager.h" + +/** + * SECTION:fbd-feedback-sound + * @short_description: Describes a feedback via a haptic motor + * @Title: FbdFeedbackSound + * + * The #FbdSoundSound describes the properties of a sound feedback + * event. It knows nothing about how to play sound itself but calls + * #FbdDevSound for that. + */ + +enum { + PROP_0, + PROP_EFFECT, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackSound { + FbdFeedbackBase parent; + + gchar *effect; +} FbdFeedbackSound; + +G_DEFINE_TYPE (FbdFeedbackSound, fbd_feedback_sound, FBD_TYPE_FEEDBACK_BASE); + +static void +on_effect_finished (FbdFeedbackSound *self) + +{ + fbd_feedback_base_done (FBD_FEEDBACK_BASE(self)); +} + +static void +fbd_feedback_sound_run (FbdFeedbackBase *base) +{ + FbdFeedbackSound *self = FBD_FEEDBACK_SOUND (base); + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevSound *sound = fbd_feedback_manager_get_dev_sound (manager); + + g_debug ("Sound event %s", self->effect); + fbd_dev_sound_play (sound, self, on_effect_finished); +} + + +static void +fbd_feedback_sound_end (FbdFeedbackBase *base) +{ + FbdFeedbackSound *self = FBD_FEEDBACK_SOUND (base); + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevSound *sound = fbd_feedback_manager_get_dev_sound (manager); + + fbd_dev_sound_stop (sound, self); +} + + +static void +fbd_feedback_sound_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackSound *self = FBD_FEEDBACK_SOUND (object); + + switch (property_id) { + case PROP_EFFECT: + self->effect = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_sound_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackSound *self = FBD_FEEDBACK_SOUND (object); + + switch (property_id) { + case PROP_EFFECT: + g_value_set_string (value, fbd_feedback_sound_get_effect (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_sound_class_init (FbdFeedbackSoundClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FbdFeedbackBaseClass *base_class = FBD_FEEDBACK_BASE_CLASS (klass); + + object_class->set_property = fbd_feedback_sound_set_property; + object_class->get_property = fbd_feedback_sound_get_property; + + base_class->run = fbd_feedback_sound_run; + base_class->end = fbd_feedback_sound_end; + + props[PROP_EFFECT] = + g_param_spec_string ( + "effect", + "Effect", + "The sound effects name", + "", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_sound_init (FbdFeedbackSound *self) +{ +} + +const char *fbd_feedback_sound_get_effect (FbdFeedbackSound *self) +{ + return self->effect; +} + +gboolean +fbd_dev_sound_stop(FbdDevSound *self, FbdFeedbackSound *feedback) +{ + /* TODO: can we use cancellable to actually end playback? */ + return TRUE; +} diff --git a/src/fbd-feedback-sound.h b/src/fbd-feedback-sound.h new file mode 100644 index 0000000..1df9c2e --- /dev/null +++ b/src/fbd-feedback-sound.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-base.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_SOUND (fbd_feedback_sound_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackSound, fbd_feedback_sound, FBD, FEEDBACK_SOUND, FbdFeedbackBase); + +const char *fbd_feedback_sound_get_effect (FbdFeedbackSound *self); + +G_END_DECLS diff --git a/src/fbd-feedback-theme.c b/src/fbd-feedback-theme.c new file mode 100644 index 0000000..018e897 --- /dev/null +++ b/src/fbd-feedback-theme.c @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-theme" + +#include "fbd-feedback-base.h" +#include "fbd-feedback-dummy.h" +#include "fbd-feedback-sound.h" +#include "fbd-feedback-theme.h" +#include "fbd-feedback-vibra.h" +#include "fbd-feedback-profile.h" + +#include + +enum { + PROP_0, + PROP_NAME, + PROP_PROFILES, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackTheme { + GObject parent; + + char *name; + GHashTable *profiles; +} FbdFeedbackTheme; + +static void json_serializable_iface_init (JsonSerializableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (FbdFeedbackTheme, fbd_feedback_theme, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (JSON_TYPE_SERIALIZABLE, + json_serializable_iface_init)); + + +static JsonNode * +fbd_theme_serializable_serialize_property (JsonSerializable *serializable, + const gchar *property_name, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (serializable); + JsonNode *node = NULL; + + if (g_strcmp0 (property_name, "profiles") == 0) { + GHashTableIter iter; + gpointer key; + FbdFeedbackProfile *profile; + JsonArray *array = json_array_sized_new (FBD_FEEDBACK_PROFILE_N_PROFILES); + + g_hash_table_iter_init (&iter, self->profiles); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *) &profile)) { + json_array_add_element (array, json_gobject_serialize (G_OBJECT(profile))); + } + node = json_node_init_array (json_node_alloc (), array); + } else { + node = json_serializable_default_serialize_property (serializable, + property_name, + value, + pspec); + } + return node; +} + +static gboolean +fbd_theme_serializable_deserialize_property (JsonSerializable *serializable, + const gchar *property_name, + GValue *value, + GParamSpec *pspec, + JsonNode *property_node) +{ + if (g_strcmp0 (property_name, "profiles") == 0) { + if (JSON_NODE_TYPE (property_node) == JSON_NODE_NULL) { + g_value_set_pointer (value, NULL); + return TRUE; + } else if (JSON_NODE_TYPE (property_node) == JSON_NODE_ARRAY) { + JsonArray *array = json_node_get_array (property_node); + guint i, array_len = json_array_get_length (array); + GHashTable *profiles = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_object_unref); + + for (i = 0; i < array_len; i++) { + JsonNode *element_node = json_array_get_element (array, i); + FbdFeedbackProfile *profile; + gchar *name; + + if (JSON_NODE_HOLDS_OBJECT (element_node)) { + profile = FBD_FEEDBACK_PROFILE (json_gobject_deserialize (FBD_TYPE_FEEDBACK_PROFILE, + element_node)); + name = g_strdup (fbd_feedback_profile_get_name (profile)); + g_hash_table_insert (profiles, name, profile); + } else { + return FALSE; + } + } + g_value_set_boxed (value, profiles); + return TRUE; + } + return FALSE; + } else { + return json_serializable_default_deserialize_property (serializable, + property_name, + value, + pspec, + property_node); + } + return FALSE; +} + +static void +fbd_feedback_theme_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); + + switch (property_id) { + case PROP_NAME: + g_free (self->name); + self->name = g_value_dup_string (value); + break; + case PROP_PROFILES: + if (self->profiles) + g_hash_table_unref (self->profiles); + self->profiles = g_value_get_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_feedback_theme_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); + + switch (property_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_PROFILES: + g_value_set_boxed (value, self->profiles); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_theme_dispose (GObject *object) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); + + g_clear_pointer (&self->profiles, g_hash_table_unref); + + G_OBJECT_CLASS (fbd_feedback_theme_parent_class)->dispose (object); +} + +static void +fbd_feedback_theme_finalize (GObject *object) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); + + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (fbd_feedback_theme_parent_class)->finalize (object); +} + +static void +json_serializable_iface_init (JsonSerializableIface *iface) +{ + iface->serialize_property = fbd_theme_serializable_serialize_property; + iface->deserialize_property = fbd_theme_serializable_deserialize_property; +} + +static void +fbd_feedback_theme_constructed (GObject *object) +{ + FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); + + G_OBJECT_CLASS (fbd_feedback_theme_parent_class)->constructed (object); + + self->profiles = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_object_unref); +} + + +static void +fbd_feedback_theme_class_init (FbdFeedbackThemeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = fbd_feedback_theme_set_property; + object_class->get_property = fbd_feedback_theme_get_property; + + object_class->constructed = fbd_feedback_theme_constructed; + object_class->dispose = fbd_feedback_theme_dispose; + object_class->finalize = fbd_feedback_theme_finalize; + + props[PROP_NAME] = + g_param_spec_string ( + "name", + "Name", + "The theme name", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_PROFILES] = + g_param_spec_boxed ( + "profiles", + "Profiles", + "The feedback profiles", + G_TYPE_HASH_TABLE, + /* Can't be CONSTRUCT_ONLY since json-glib can't handle it */ + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_theme_init (FbdFeedbackTheme *self) +{ +} + +FbdFeedbackTheme * +fbd_feedback_theme_new (const gchar *name) +{ + return FBD_FEEDBACK_THEME (g_object_new (FBD_TYPE_FEEDBACK_THEME, + "name", name, + NULL)); +} + + +FbdFeedbackTheme * +fbd_feedback_theme_new_from_data (const gchar *data, GError **error) +{ + JsonNode *node = json_from_string(data, error); + if (!node) + return NULL; + + return FBD_FEEDBACK_THEME (json_gobject_deserialize (FBD_TYPE_FEEDBACK_THEME, node)); +} + + +FbdFeedbackTheme * +fbd_feedback_theme_new_from_file (const gchar *filename, GError **error) +{ + g_autofree char *data = NULL; + + if (!g_file_get_contents (filename, &data, NULL, error)) + return NULL; + + return fbd_feedback_theme_new_from_data (data, error); +} + +const char * +fbd_feedback_theme_get_name (FbdFeedbackTheme *self) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_THEME (self), ""); + + return self->name; +} + +void +fbd_feedback_theme_add_profile (FbdFeedbackTheme *self, FbdFeedbackProfile *profile) +{ + gchar *name; + + g_return_if_fail (FBD_IS_FEEDBACK_THEME (self)); + g_return_if_fail (FBD_IS_FEEDBACK_PROFILE (profile)); + name = g_strdup (fbd_feedback_profile_get_name (profile)); + + g_hash_table_insert (self->profiles, name, g_object_ref (profile)); +} + +FbdFeedbackProfile * +fbd_feedback_theme_get_profile (FbdFeedbackTheme *self, const char *name) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_THEME (self), NULL); + + return g_hash_table_lookup (self->profiles, name); +} + +GSList * +fbd_feedback_theme_lookup_feedback (FbdFeedbackTheme *self, + FbdFeedbackProfileLevel level, + FbdEvent *event) +{ + GSList *feedbacks = NULL; + + for (int i = level; i >= FBD_FEEDBACK_PROFILE_LEVEL_SILENT; i--) { + const char *profile_name = fbd_feedback_profile_level_to_string (i); + FbdFeedbackProfile *profile = fbd_feedback_theme_get_profile (self, profile_name); + FbdFeedbackBase *feedback = fbd_feedback_profile_get_feedback (profile, + fbd_event_get_event (event)); + if (feedback) { + feedbacks = g_slist_prepend (feedbacks, g_object_ref(feedback)); + } + } + + if (!g_slist_length (feedbacks)) + g_warning ("No feedback for event %s", fbd_event_get_event (event)); + return feedbacks; +} diff --git a/src/fbd-feedback-theme.h b/src/fbd-feedback-theme.h new file mode 100644 index 0000000..f9ed3ff --- /dev/null +++ b/src/fbd-feedback-theme.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-event.h" +#include "fbd-feedback-base.h" +#include "fbd-feedback-manager.h" +#include "fbd-feedback-profile.h" + +#include + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_THEME (fbd_feedback_theme_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackTheme, fbd_feedback_theme, FBD, FEEDBACK_THEME, GObject); + +FbdFeedbackTheme *fbd_feedback_theme_new (const char *name); +FbdFeedbackTheme *fbd_feedback_theme_new_from_data (const gchar *data, GError **error); +FbdFeedbackTheme *fbd_feedback_theme_new_from_file (const gchar *filename, GError **error); + +const char *fbd_feedback_theme_get_name (FbdFeedbackTheme *self); +void fbd_feedback_theme_add_profile (FbdFeedbackTheme *self, + FbdFeedbackProfile *profile); +FbdFeedbackProfile *fbd_feedback_theme_get_profile (FbdFeedbackTheme *self, const char *name); + +GSList *fbd_feedback_theme_lookup_feedback (FbdFeedbackTheme *self, + FbdFeedbackProfileLevel profile, + FbdEvent *event); + +G_END_DECLS diff --git a/src/fbd-feedback-vibra-periodic.c b/src/fbd-feedback-vibra-periodic.c new file mode 100644 index 0000000..2926ba1 --- /dev/null +++ b/src/fbd-feedback-vibra-periodic.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-vibra-periodic" + +#include "fbd-enums.h" +#include "fbd-feedback-vibra-periodic.h" +#include "fbd-feedback-manager.h" + +/** + * SECTION:fbd-feedback-vibra + * @short_description: Describes a feedback via a haptic motor + * @Title: FbdFeedbackVibra + * + * The #FbdVibraVibra describes the properties of a haptic feedback + * event. It knows nothing about the hardware itself but calls + * #FbdDevVibra for that. + */ + +enum { + PROP_0, + PROP_MAGNITUDE, + PROP_FADE_IN_LEVEL, + PROP_FADE_IN_TIME, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackVibraPeriodic { + FbdFeedbackVibra parent; + + guint magnitude; + guint fade_in_level; + guint fade_in_time; +} FbdFeedbackVibraPeriodic; + +G_DEFINE_TYPE (FbdFeedbackVibraPeriodic, fbd_feedback_vibra_periodic, FBD_TYPE_FEEDBACK_VIBRA); + +static void +fbd_feedback_vibra_periodic_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibraPeriodic *self = FBD_FEEDBACK_VIBRA_PERIODIC (object); + + switch (property_id) { + case PROP_MAGNITUDE: + self->magnitude = g_value_get_uint (value); + break; + case PROP_FADE_IN_LEVEL: + self->fade_in_level = g_value_get_uint (value); + break; + case PROP_FADE_IN_TIME: + self->fade_in_time = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_vibra_periodic_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibraPeriodic *self = FBD_FEEDBACK_VIBRA_PERIODIC (object); + + switch (property_id) { + case PROP_MAGNITUDE: + g_value_set_uint (value, self->magnitude); + break; + case PROP_FADE_IN_LEVEL: + g_value_set_uint (value, self->fade_in_level); + break; + case PROP_FADE_IN_TIME: + g_value_set_uint (value, self->fade_in_time); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_vibra_periodic_end_vibra (FbdFeedbackVibra *vibra) +{ + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + + fbd_dev_vibra_stop (dev); +} + +static void +fbd_feedback_vibra_periodic_start_vibra (FbdFeedbackVibra *vibra) +{ + FbdFeedbackVibraPeriodic *self = FBD_FEEDBACK_VIBRA_PERIODIC (vibra); + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + guint duration = fbd_feedback_vibra_get_duration (vibra); + + g_debug ("Periodic Vibra: %d %d %d %d", + duration, self->magnitude, self->fade_in_level, self->fade_in_time); + + fbd_dev_vibra_periodic (dev, duration, self->magnitude, self->fade_in_level, + self->fade_in_time); +} + +static void +fbd_feedback_vibra_periodic_class_init (FbdFeedbackVibraPeriodicClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FbdFeedbackVibraClass *vibra_class = FBD_FEEDBACK_VIBRA_CLASS (klass); + + object_class->set_property = fbd_feedback_vibra_periodic_set_property; + object_class->get_property = fbd_feedback_vibra_periodic_get_property; + + vibra_class->start_vibra = fbd_feedback_vibra_periodic_start_vibra; + vibra_class->end_vibra = fbd_feedback_vibra_periodic_end_vibra; + + props[PROP_MAGNITUDE] = + g_param_spec_uint ( + "magnitude", + "Magnitude", + "total magnitude", + 0, 0xFFFF, 0x7FFF, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_FADE_IN_TIME] = + g_param_spec_uint ( + "fade-in-time", + "Fade in time", + "Fade in time until full strength", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_FADE_IN_LEVEL] = + g_param_spec_uint ( + "fade-in-level", + "Fade in level", + "Fade in start level", + 0, 0xFFFF, 0x7FFF, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_vibra_periodic_init (FbdFeedbackVibraPeriodic *self) +{ +} diff --git a/src/fbd-feedback-vibra-periodic.h b/src/fbd-feedback-vibra-periodic.h new file mode 100644 index 0000000..fccc19c --- /dev/null +++ b/src/fbd-feedback-vibra-periodic.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-vibra.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_VIBRA_PERIODIC (fbd_feedback_vibra_periodic_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackVibraPeriodic, fbd_feedback_vibra_periodic, FBD, + FEEDBACK_VIBRA_PERIODIC, + FbdFeedbackVibra); + +G_END_DECLS diff --git a/src/fbd-feedback-vibra-rumble.c b/src/fbd-feedback-vibra-rumble.c new file mode 100644 index 0000000..87e3910 --- /dev/null +++ b/src/fbd-feedback-vibra-rumble.c @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-vibra-rumble" + +#include "fbd-enums.h" +#include "fbd-feedback-vibra-rumble.h" +#include "fbd-feedback-manager.h" + +/** + * SECTION:fbd-feedback-vibra + * @short_description: Describes a feedback via a haptic motor + * @Title: FbdFeedbackVibra + * + * The #FbdVibraVibra describes the properties of a haptic feedback + * event. It knows nothing about the hardware itself but calls + * #FbdDevVibra for that. + */ + +enum { + PROP_0, + PROP_COUNT, + PROP_PAUSE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackVibraRumble { + FbdFeedbackVibra parent; + + guint count; /* number of rumbles */ + guint pause; /* pause in msecs */ + + guint rumble; /* rumble in msecs */ + guint periods; /* number of periods to play */ + guint timer_id; +} FbdFeedbackVibraRumble; + +G_DEFINE_TYPE (FbdFeedbackVibraRumble, fbd_feedback_vibra_rumble, FBD_TYPE_FEEDBACK_VIBRA); + +static void +fbd_feedback_vibra_rumble_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibraRumble *self = FBD_FEEDBACK_VIBRA_RUMBLE (object); + + switch (property_id) { + case PROP_COUNT: + self->count = g_value_get_uint (value); + break; + case PROP_PAUSE: + self->pause = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_vibra_rumble_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibraRumble *self = FBD_FEEDBACK_VIBRA_RUMBLE (object); + + switch (property_id) { + case PROP_COUNT: + g_value_set_uint (value, self->count); + break; + case PROP_PAUSE: + g_value_set_uint (value, self->pause); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +on_period_ended (FbdFeedbackVibraRumble *self) +{ + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + + g_return_val_if_fail (FBD_IS_FEEDBACK_VIBRA_RUMBLE (self), G_SOURCE_REMOVE); + + if (self->periods) { + fbd_dev_vibra_rumble (dev, self->rumble, FALSE); + self->periods--; + return G_SOURCE_CONTINUE; + } + return G_SOURCE_REMOVE; +} + +static void +fbd_feedback_vibra_rumble_end_vibra (FbdFeedbackVibra *vibra) +{ + FbdFeedbackVibraRumble *self = FBD_FEEDBACK_VIBRA_RUMBLE (vibra); + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + + fbd_dev_vibra_stop (dev); + g_clear_handle_id(&self->timer_id, g_source_remove); +} + +static void +fbd_feedback_vibra_rumble_start_vibra (FbdFeedbackVibra *vibra) +{ + FbdFeedbackVibraRumble *self = FBD_FEEDBACK_VIBRA_RUMBLE (vibra); + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + guint duration = fbd_feedback_vibra_get_duration (vibra); + guint period; + + self->rumble = (duration / self->count) - self->pause; + if (self->rumble <= 0) { + self->rumble = FBD_FEEDBACK_VIBRA_DEFAULT_DURATION; + self->pause = 0; + self->count = 1; + } + period = self->rumble + self->pause; + self->periods = self->count; + + g_debug ("Rumble Vibra event: duration %d, rumble: %d, pause: %d, period: %d", + duration, self->rumble, self->pause, period); + fbd_dev_vibra_rumble (dev, self->rumble, TRUE); + self->periods--; + if (self->periods) { + self->timer_id = g_timeout_add (period, (GSourceFunc) on_period_ended, self); + } +} + +static void +fbd_feedback_vibra_rumble_class_init (FbdFeedbackVibraRumbleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FbdFeedbackVibraClass *vibra_class = FBD_FEEDBACK_VIBRA_CLASS (klass); + + object_class->set_property = fbd_feedback_vibra_rumble_set_property; + object_class->get_property = fbd_feedback_vibra_rumble_get_property; + + vibra_class->start_vibra = fbd_feedback_vibra_rumble_start_vibra; + vibra_class->end_vibra = fbd_feedback_vibra_rumble_end_vibra; + + props[PROP_COUNT] = + g_param_spec_uint ( + "count", + "Count", + "The number of rumbles", + 0, G_MAXINT, 1, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_PAUSE] = + g_param_spec_uint ( + "pause", + "Pause", + "The pause in msecs between rumbles", + 0, G_MAXINT, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_vibra_rumble_init (FbdFeedbackVibraRumble *self) +{ + self->count = 1; +} diff --git a/src/fbd-feedback-vibra-rumble.h b/src/fbd-feedback-vibra-rumble.h new file mode 100644 index 0000000..1a1cf1b --- /dev/null +++ b/src/fbd-feedback-vibra-rumble.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-vibra.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_VIBRA_RUMBLE (fbd_feedback_vibra_rumble_get_type()) + +G_DECLARE_FINAL_TYPE (FbdFeedbackVibraRumble, fbd_feedback_vibra_rumble, FBD, + FEEDBACK_VIBRA_RUMBLE, + FbdFeedbackVibra); + +G_END_DECLS diff --git a/src/fbd-feedback-vibra.c b/src/fbd-feedback-vibra.c new file mode 100644 index 0000000..52b6d69 --- /dev/null +++ b/src/fbd-feedback-vibra.c @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-feedback-vibra" + +#include "fbd-enums.h" +#include "fbd-feedback-vibra.h" +#include "fbd-feedback-manager.h" + +/** + * SECTION:fbd-feedback-vibra + * @short_description: Describes a feedback via a haptic motor + * @Title: FbdFeedbackVibra + * + * The #FbdVibraVibra describes the properties of a haptic feedback + * event. It knows nothing about the hardware itself but calls + * #FbdDevVibra for that. + */ + +enum { + PROP_0, + PROP_DURATION, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _FbdFeedbackVibraPrivate { + guint duration; + guint timer_id; +} FbdFeedbackVibraPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (FbdFeedbackVibra, fbd_feedback_vibra, FBD_TYPE_FEEDBACK_BASE); + + +static gboolean +on_timeout_expired (FbdFeedbackVibra *self) +{ + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + FbdDevVibra *dev = fbd_feedback_manager_get_dev_vibra (manager); + + fbd_dev_vibra_remove_effect (dev); + fbd_feedback_base_done (FBD_FEEDBACK_BASE(self)); + return G_SOURCE_REMOVE; +} + +static void +fbd_feedback_vibra_run (FbdFeedbackBase *base) +{ + FbdFeedbackVibra *self = FBD_FEEDBACK_VIBRA (base); + FbdFeedbackVibraPrivate *priv = fbd_feedback_vibra_get_instance_private (self); + FbdFeedbackVibraClass *klass; + + klass = FBD_FEEDBACK_VIBRA_GET_CLASS (self); + g_return_if_fail (klass->start_vibra); + klass->start_vibra (self); + + priv->timer_id = g_timeout_add (priv->duration, + (GSourceFunc)on_timeout_expired, + self); + g_source_set_name_by_id (priv->timer_id, "feedback-vibra-timer"); +} + + +static void +fbd_feedback_vibra_end (FbdFeedbackBase *base) +{ + FbdFeedbackVibra *self = FBD_FEEDBACK_VIBRA (base); + FbdFeedbackVibraPrivate *priv = fbd_feedback_vibra_get_instance_private (self); + FbdFeedbackVibraClass *klass = FBD_FEEDBACK_VIBRA_GET_CLASS (self); + + if (!priv->timer_id) + return; + + g_return_if_fail (klass->end_vibra); + klass->end_vibra(self); + g_clear_handle_id(&priv->timer_id, g_source_remove); + fbd_feedback_base_done (FBD_FEEDBACK_BASE(self)); +} + + +static void +fbd_feedback_vibra_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibra *self = FBD_FEEDBACK_VIBRA (object); + FbdFeedbackVibraPrivate *priv = fbd_feedback_vibra_get_instance_private (self); + + switch (property_id) { + case PROP_DURATION: + priv->duration = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_vibra_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdFeedbackVibra *self = FBD_FEEDBACK_VIBRA (object); + FbdFeedbackVibraPrivate *priv = fbd_feedback_vibra_get_instance_private (self); + + switch (property_id) { + case PROP_DURATION: + g_value_set_uint (value, priv->duration); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +fbd_feedback_vibra_class_init (FbdFeedbackVibraClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FbdFeedbackBaseClass *base_class = FBD_FEEDBACK_BASE_CLASS (klass); + + object_class->set_property = fbd_feedback_vibra_set_property; + object_class->get_property = fbd_feedback_vibra_get_property; + + base_class->run = fbd_feedback_vibra_run; + base_class->end = fbd_feedback_vibra_end; + + props[PROP_DURATION] = + g_param_spec_uint ( + "duration", + "Duration", + "Vibra event duration in msecs", + 0, G_MAXUINT, FBD_FEEDBACK_VIBRA_DEFAULT_DURATION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + +static void +fbd_feedback_vibra_init (FbdFeedbackVibra *self) +{ +} + +guint fbd_feedback_vibra_get_duration (FbdFeedbackVibra *self) +{ + FbdFeedbackVibraPrivate *priv; + + g_return_val_if_fail (FBD_IS_FEEDBACK_VIBRA (self), 0); + priv = fbd_feedback_vibra_get_instance_private (self); + return priv->duration; +} diff --git a/src/fbd-feedback-vibra.h b/src/fbd-feedback-vibra.h new file mode 100644 index 0000000..f3e2515 --- /dev/null +++ b/src/fbd-feedback-vibra.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0+ + */ +#pragma once + +#include "fbd-feedback-base.h" + +G_BEGIN_DECLS + +#define FBD_TYPE_FEEDBACK_VIBRA (fbd_feedback_vibra_get_type()) + +#define FBD_FEEDBACK_VIBRA_DEFAULT_DURATION 1000 + +G_DECLARE_DERIVABLE_TYPE (FbdFeedbackVibra, fbd_feedback_vibra, FBD, FEEDBACK_VIBRA, FbdFeedbackBase); + +struct _FbdFeedbackVibraClass +{ + FbdFeedbackBaseClass parent_class; + + void (*start_vibra) (FbdFeedbackVibra *self); + void (*end_vibra) (FbdFeedbackVibra *self); +}; + +guint fbd_feedback_vibra_get_duration (FbdFeedbackVibra *self); + +G_END_DECLS diff --git a/src/fbd.c b/src/fbd.c new file mode 100644 index 0000000..8040d00 --- /dev/null +++ b/src/fbd.c @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd" + +#include "fbd.h" +#include "fbd-feedback-manager.h" +#include "lfb-names.h" +#include "lfb-gdbus.h" + +#include +#include + + +static GMainLoop *loop; + + +GQuark fbd_error_quark(void) +{ + static GQuark quark = 0; + + if (!quark) + quark = g_quark_from_static_string("fbd"); + + return quark; +} + + +static gboolean +quit_cb (gpointer user_data) +{ + g_info ("Caught signal, shutting down..."); + + if (loop) + g_idle_add ((GSourceFunc) g_main_loop_quit, loop); + else + exit (0); + + return FALSE; +} + + +static void +bus_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + FbdFeedbackManager *manager = fbd_feedback_manager_get_default (); + + g_debug ("Bus acquired, creating manager..."); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (manager), + connection, + FB_DBUS_PATH, + NULL); +} + + +static void +name_acquired_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("Service name '%s' was acquired", name); +} + +static void +name_lost_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + /* Note that we're not allowing replacement, so once the name acquired, the + * process won't lose it. */ + if (!name) { + g_warning ("Could not get the session bus. Make sure " + "the message bus daemon is running!"); + } else { + if (connection) + g_warning ("Could not acquire the '%s' service name", name); + else + g_debug ("DBus connection close"); + } + + g_main_loop_quit (loop); +} + + +int main(int argc, char *argv[]) +{ + g_autoptr(GError) err = NULL; + g_autoptr(GOptionContext) opt_context = NULL; + + opt_context = g_option_context_new ("- A daemon to trigger event feedback"); + if (!g_option_context_parse (opt_context, &argc, &argv, &err)) { + g_warning ("%s", err->message); + g_clear_error (&err); + return 1; + } + + g_unix_signal_add (SIGTERM, quit_cb, NULL); + g_unix_signal_add (SIGINT, quit_cb, NULL); + + loop = g_main_loop_new (NULL, FALSE); + + g_bus_own_name (FB_DBUS_TYPE, + FB_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_NONE, + bus_acquired_cb, + name_acquired_cb, + name_lost_cb, + NULL, + NULL); + + g_main_loop_run (loop); + g_main_loop_unref (loop); +} diff --git a/src/fbd.h b/src/fbd.h new file mode 100644 index 0000000..edeb1fe --- /dev/null +++ b/src/fbd.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +typedef enum { + FBD_ERROR_FAILED = 0, +} FbdError; + +GQuark fbd_error_quark (void); + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..ade8d58 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,68 @@ +# Feedback daemon +if get_option('daemon') + +fbd_enum_headers = files([ + 'fbd-event.h', + 'fbd-feedback-vibra.h', +]) +fbd_enum_sources = gnome.mkenums_simple('fbd-enums', + sources : fbd_enum_headers) + +sources = [ + generated_dbus_sources, + fbd_enum_sources, + 'fbd.c', + 'fbd-dev-vibra.c', + 'fbd-dev-sound.c', + 'fbd-event.c', + 'fbd-feedback-base.c', + 'fbd-feedback-dummy.c', + 'fbd-feedback-manager.c', + 'fbd-feedback-profile.c', + 'fbd-feedback-sound.c', + 'fbd-feedback-theme.c', + 'fbd-feedback-vibra.c', + 'fbd-feedback-vibra-periodic.c', + 'fbd-feedback-vibra-rumble.c', +] + +fbd_deps = [ + gio, + gio_unix, + glib, + gobject, + gsound, + gudev, + json_glib, +] + +fbd_inc = [ + include_directories('.'), + libfeedback_inc, + dbus_inc, +] + +fbd_lib = static_library( + 'fbd', + sources, + include_directories : fbd_inc, + dependencies : fbd_deps, + install : false, +) + +fbd_dep = declare_dependency( + include_directories : fbd_inc, + link_with : fbd_lib, + dependencies : fbd_deps, +) + +executable( + 'feedbackd', + sources : 'fbd.c', + include_directories : fbd_inc, + dependencies : fbd_dep, + install : true, + install_dir: libexecdir, +) + +endif diff --git a/tests/data/test.json b/tests/data/test.json new file mode 100644 index 0000000..067249a --- /dev/null +++ b/tests/data/test.json @@ -0,0 +1,36 @@ +{ + "name" : "default", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-0" + }, + { + "type" : "Dummy", + "event-name" : "test-dummy-10", + "duration" : 10000 + } + ] + }, + { + "name" : "quiet", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-0" + }, + { + "type" : "Dummy", + "event-name" : "test-dummy-10", + "duration" : 10000 + } + ] + }, + { + "name" : "silent" + } + ] +} diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..2de587c --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,86 @@ +if get_option('tests') + +test_env = environment() +test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) +test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) +test_env.set('G_DEBUG', 'gc-friendly,fatal-warnings') +test_env.set('GSETTINGS_BACKEND', 'memory') +test_env.set('GSETTINGS_SCHEMA_DIR', '@0@/data'.format(meson.build_root())) +test_env.set('PYTHONDONTWRITEBYTECODE', 'yes') +test_env.set('MALLOC_CHECK_', '2') +test_env.set('XDG_CONFIG_HOME', meson.current_source_dir()) +test_env.set('XDG_CONFIG_DIRS', meson.current_source_dir()) + +# Shared lfbrary Unit tests + +test_lfb_cflags = [ + '-DTEST_APP_ID="org.sigxcpu.feedbackd_test"', + '-DLIBFEEDBACK_USE_UNSTABLE_API', + '-DTEST_DATA_DIR="@0@"'.format(join_paths(meson.current_source_dir(), 'data')), +] + +test_lfb_link_args = [ + '-fPIC', +] + +test_lfb_deps = [ + libfeedback_dep, + glib, + gio_unix, +] + +lfb_tests = [ + 'lfb-event', +] + +if get_option ('daemon') + # The integration test needs the daemon + lfb_tests += 'lfb-integration' + subdir('services') +endif + +foreach test : lfb_tests + t = executable('test-@0@'.format(test), + ['test-@0@.c'.format(test)], + c_args : test_lfb_cflags, + pie : true, + link_args : test_lfb_link_args, + dependencies : test_lfb_deps) + test(test, t, env: test_env) +endforeach + +# Daemon Unit tests +if get_option('daemon') + +test_fbd_cflags = [ + '-DTEST_APP_ID="org.sigxcpu.feedbackd_test"', +] + +test_fbd_link_args = [ + '-fPIC', +] + +test_fbd_deps = [ + fbd_dep, +] + +fbd_tests = [ + 'fbd-feedback-profile', + 'fbd-feedback-theme', +] + +foreach test : fbd_tests + t = executable('test-@0@'.format(test), + ['test-@0@.c'.format(test)], + c_args : test_fbd_cflags, + pie : true, + link_args : test_fbd_link_args, + include_directories : fbd_inc, + dependencies : test_fbd_deps) + test(test, t, env : test_env) +endforeach + + +endif # daemon + +endif diff --git a/tests/services/meson.build b/tests/services/meson.build new file mode 100644 index 0000000..06a1f4f --- /dev/null +++ b/tests/services/meson.build @@ -0,0 +1,9 @@ +service_file = 'org.sigxcpu.Feedback.service' +service_data = configuration_data() +service_data.set('ABS_BUILDDIR', meson.build_root()) + +configure_file( + input : service_file + '.in', + output : service_file, + configuration : service_data +) diff --git a/tests/services/org.sigxcpu.Feedback.service.in b/tests/services/org.sigxcpu.Feedback.service.in new file mode 100644 index 0000000..43577c3 --- /dev/null +++ b/tests/services/org.sigxcpu.Feedback.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.sigxcpu.Feedback +Exec=@ABS_BUILDDIR@/run + diff --git a/tests/test-fbd-event.c b/tests/test-fbd-event.c new file mode 100644 index 0000000..e4e9181 --- /dev/null +++ b/tests/test-fbd-event.c @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#include "fbd-event.h" +#include "fbd-feedback-dummy.h" + +#define TEST_EVENT "window-close" + +static void +test_fbd_event (void) +{ + g_autoptr(FbdEvent) event = NULL; + g_autofree gchar *appid = NULL; + g_autofree gchar *name = NULL; + FbdEventEndReason reason; + gint timeout; + + event = fbd_event_new (1, TEST_APP_ID, TEST_EVENT, 2); + + g_assert_true (FBD_IS_EVENT (event)); + g_object_get (event, + "end-reason", &reason, + "event", &name, + "app-id", &appid, + "timeout", &timeout, + NULL); + + g_assert_cmpstr (fbd_event_get_event (event), ==, TEST_EVENT); + g_assert_cmpstr (name, ==, TEST_EVENT); + + g_assert_cmpstr (fbd_event_get_app_id (event), ==, TEST_APP_ID); + g_assert_cmpstr (appid, ==, TEST_APP_ID); + + g_assert_cmpint (fbd_event_get_timeout (event), ==, timeout); + g_assert_cmpint (timeout, ==, 2); + + g_assert_cmpint (fbd_event_get_end_reason (event), ==, FBD_EVENT_END_REASON_NORMAL); + g_assert_cmpint (reason, ==, FBD_EVENT_END_REASON_NORMAL); +} + +static void +test_fbd_event_feedback (void) +{ + GSList *feedbacks; + g_autoptr(FbdEvent) event = NULL; + g_autoptr(FbdFeedbackDummy) feedback1 = NULL; + g_autoptr(FbdFeedbackDummy) feedback2 = NULL; + + event = fbd_event_new (1, TEST_APP_ID, TEST_EVENT, -1); + feedback1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + feedback2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + + feedbacks = fbd_event_get_feedbacks (event); + g_assert_cmpint (g_slist_length (feedbacks), ==, 0); + + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + feedbacks = fbd_event_get_feedbacks (event); + g_assert_cmpint (g_slist_length (feedbacks), ==, 1); + + /* Remove non existing feedback */ + fbd_event_remove_feedback (event, FBD_FEEDBACK_BASE(feedback2)); + feedbacks = fbd_event_get_feedbacks (event); + g_assert_cmpint (g_slist_length (feedbacks), ==, 1); + + fbd_event_remove_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + feedbacks = fbd_event_get_feedbacks (event); + g_assert_cmpint (g_slist_length (feedbacks), ==, 0); + + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback2)); + feedbacks = fbd_event_get_feedbacks (event); + g_assert_cmpint (g_slist_length (feedbacks), ==, 2); + + g_assert_false (fbd_event_get_feedbacks_ended (event)); + fbd_event_end_feedbacks (event); + /* Dummy feedback ends immediately */ + g_assert_true (fbd_event_get_feedbacks_ended (event)); +} + +static void +on_feedbacks_ended (FbdEvent *event, gboolean *ended) +{ + FBD_IS_EVENT (event); + + *ended = TRUE; +} + +static void +test_fbd_event_feedback_ended (void) +{ + g_autoptr(FbdEvent) event = NULL; + g_autoptr(FbdFeedbackDummy) feedback1 = NULL; + g_autoptr(FbdFeedbackDummy) feedback2 = NULL; + gboolean ended = FALSE; + + event = fbd_event_new (1, TEST_APP_ID, TEST_EVENT, FBD_EVENT_TIMEOUT_ONESHOT); + feedback1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + feedback2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback2)); + + g_signal_connect (event, "feedbacks-ended", + (GCallback)on_feedbacks_ended, &ended); + + fbd_event_end_feedbacks (event); + g_assert_true (ended); +} + +static void +test_fbd_event_feedback_loop (void) +{ + g_autoptr(FbdEvent) event = NULL; + g_autoptr(FbdFeedbackDummy) feedback1 = NULL; + g_autoptr(FbdFeedbackDummy) feedback2 = NULL; + gboolean ended = FALSE; + + event = fbd_event_new (1, TEST_APP_ID, TEST_EVENT, FBD_EVENT_TIMEOUT_LOOP); + feedback1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + feedback2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback2)); + + g_signal_connect (event, "feedbacks-ended", + (GCallback)on_feedbacks_ended, &ended); + g_assert_false (ended); + + fbd_event_end_feedbacks (event); + g_assert_true (ended); +} + +static void +test_fbd_event_feedback_timeout (void) +{ + g_autoptr(FbdEvent) event = NULL; + g_autoptr(FbdFeedbackDummy) feedback1 = NULL; + g_autoptr(FbdFeedbackDummy) feedback2 = NULL; + gboolean ended = FALSE; + + event = fbd_event_new (1, TEST_APP_ID, TEST_EVENT, 1); + feedback1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback1)); + feedback2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, NULL); + fbd_event_add_feedback (event, FBD_FEEDBACK_BASE(feedback2)); + + g_signal_connect (event, "feedbacks-ended", + (GCallback)on_feedbacks_ended, &ended); + g_assert_false (ended); + + fbd_event_end_feedbacks (event); + g_assert_true (ended); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func("/feedbackd/fbd/event/new", test_fbd_event); + g_test_add_func("/feedbackd/fbd/event/feedbacks/props", test_fbd_event_feedback); + g_test_add_func("/feedbackd/fbd/event/feedbacks/ended", test_fbd_event_feedback_ended); + g_test_add_func("/feedbackd/fbd/event/feedbacks/loop", test_fbd_event_feedback_loop); + g_test_add_func("/feedbackd/fbd/event/feedbacks/timeout", test_fbd_event_feedback_timeout); + + return g_test_run(); +} diff --git a/tests/test-fbd-feedback-profile.c b/tests/test-fbd-feedback-profile.c new file mode 100644 index 0000000..bb7cc5c --- /dev/null +++ b/tests/test-fbd-feedback-profile.c @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#include "fbd-feedback-profile.h" +#include "fbd-feedback-dummy.h" +#include "fbd-feedback-vibra.h" + +#include + +#define PROFILE_NAME "test" + +static void +test_fbd_feedback_profile_name (void) +{ + g_autoptr(FbdFeedbackProfile) profile = fbd_feedback_profile_new (PROFILE_NAME); + + g_assert_true (FBD_IS_FEEDBACK_PROFILE (profile)); + + g_assert_cmpstr (fbd_feedback_profile_get_name (profile), ==, PROFILE_NAME); +} + +static void +test_fbd_feedback_profile_feedbacks (void) +{ + GHashTable *feedbacks; + FbdFeedbackBase *fb; + g_autoptr (FbdFeedbackDummy) fb1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event1", + NULL); + g_autoptr (FbdFeedbackDummy) fb2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event2", + NULL); + g_autoptr(FbdFeedbackProfile) profile = fbd_feedback_profile_new (PROFILE_NAME); + + g_assert_true (FBD_IS_FEEDBACK_PROFILE (profile)); + g_object_get (profile, "feedbacks", &feedbacks, NULL); + g_assert_nonnull (feedbacks); + g_hash_table_unref (feedbacks); + fbd_feedback_profile_add_feedback (profile, FBD_FEEDBACK_BASE(fb1)); + fbd_feedback_profile_add_feedback (profile, FBD_FEEDBACK_BASE(fb2)); + + fb = fbd_feedback_profile_get_feedback (profile, "event1"); + g_assert_cmpstr (fbd_feedback_get_event_name (fb), ==, "event1"); + + fb = fbd_feedback_profile_get_feedback (profile, "does-not-exist"); + g_assert_null (fb); +} + +static void +test_fbd_feedback_profile_parse (void) +{ + const char *json =" " + " { " + " \"name\" : \"full\", " + " \"feedbacks\" : [ " + " { " + " \"type\" : \"vibra\", " + " \"event-name\" : \"event1\" " + " }, " + " { " + " \"type\" : \"dummy\", " + " \"event-name\" : \"event2\" " + " } " + " ] " + " } "; + g_autoptr (GError) err = NULL; + g_autoptr (FbdFeedbackProfile) profile = NULL; + JsonNode *node; + FbdFeedbackBase *fb; + + node = json_from_string(json, &err); + g_assert_no_error (err); + profile = FBD_FEEDBACK_PROFILE (json_gobject_deserialize (FBD_TYPE_FEEDBACK_PROFILE, node)); + g_assert_nonnull (profile); + fb = fbd_feedback_profile_get_feedback (profile, "event2"); + g_assert_true (FBD_IS_FEEDBACK_DUMMY(fb)); + fb = fbd_feedback_profile_get_feedback (profile, "event1"); + g_assert_true (FBD_IS_FEEDBACK_VIBRA(fb)); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func("/feedbackd/fbd/feedback-profile/name", test_fbd_feedback_profile_name); + g_test_add_func("/feedbackd/fbd/feedback-profile/feedbacks", test_fbd_feedback_profile_feedbacks); + g_test_add_func("/feedbackd/fbd/feedback-profile/parse", test_fbd_feedback_profile_parse); + + return g_test_run(); +} diff --git a/tests/test-fbd-feedback-theme.c b/tests/test-fbd-feedback-theme.c new file mode 100644 index 0000000..9b333f4 --- /dev/null +++ b/tests/test-fbd-feedback-theme.c @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#include "fbd-feedback-dummy.h" +#include "fbd-feedback-theme.h" + +#include + +#define THEME_NAME "test" + +static void +test_fbd_feedback_theme_name (void) +{ + g_autoptr(FbdFeedbackTheme) theme = fbd_feedback_theme_new (THEME_NAME); + + g_assert_true (FBD_IS_FEEDBACK_THEME (theme)); + + g_assert_cmpstr (fbd_feedback_theme_get_name (theme), ==, THEME_NAME); +} + +static void +test_fbd_feedback_theme_profiles (void) +{ + g_autoptr (FbdFeedbackDummy) quiet_fb1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event1", + NULL); + g_autoptr (FbdFeedbackDummy) quiet_fb2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event2", + NULL); + g_autoptr (FbdFeedbackDummy) full_fb1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event1", + NULL); + g_autoptr (FbdFeedbackDummy) full_fb2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "event2", + NULL); + g_autoptr(FbdFeedbackTheme) theme = fbd_feedback_theme_new (THEME_NAME); + + FbdFeedbackProfile *profile_full = fbd_feedback_profile_new ("full"); + FbdFeedbackProfile *profile_quiet = fbd_feedback_profile_new ("quiet"); + FbdFeedbackProfile *profile; + + fbd_feedback_profile_add_feedback (profile_quiet, FBD_FEEDBACK_BASE(quiet_fb1)); + fbd_feedback_profile_add_feedback (profile_quiet, FBD_FEEDBACK_BASE(quiet_fb2)); + + fbd_feedback_profile_add_feedback (profile_full, FBD_FEEDBACK_BASE(full_fb1)); + fbd_feedback_profile_add_feedback (profile_full, FBD_FEEDBACK_BASE(full_fb2)); + + g_assert_true (FBD_IS_FEEDBACK_THEME (theme)); + fbd_feedback_theme_add_profile (theme, profile_quiet); + fbd_feedback_theme_add_profile (theme, profile_full); + + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE (profile)); + + g_print ("%s\n", json_gobject_to_data (G_OBJECT(theme), NULL)); +} + + +static void +test_fbd_feedback_theme_parse (void) +{ + const char *json =" " + "{ " + " \"name\" : \"test\", " + " \"profiles\" : [ " + " { " + " \"name\" : \"full\", " + " \"feedbacks\" : [ " + " { " + " \"type\" : \"dummy\", " + " \"event-name\" : \"event1\" " + " }, " + " { " + " \"type\" : \"dummy\", " + " \"event-name\" : \"event2\" " + " } " + " ] " + " }, " + " { " + " \"name\" : \"quiet\", " + " \"feedbacks\" : [ " + " { " + " \"type\" : \"dummy\", " + " \"event-name\" : \"event1\" " + " }, " + " { " + " \"type\" : \"dummy\", " + " \"event-name\" : \"event2\" " + " } " + " ] " + " } " + " ] " + "} "; + g_autoptr (GError) err = NULL; + g_autoptr (FbdFeedbackTheme) theme = NULL; + FbdFeedbackProfile *profile; + + theme = fbd_feedback_theme_new_from_data (json, &err); + g_assert_no_error (err); + g_assert_nonnull (theme); + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + g_assert_cmpstr ("full", ==, + fbd_feedback_profile_get_name (profile)); + profile = fbd_feedback_theme_get_profile (theme, "quiet"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + g_assert_cmpstr ("quiet", ==, + fbd_feedback_profile_get_name (profile)); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func("/feedbackd/fbd/feedback-theme/name", test_fbd_feedback_theme_name); + g_test_add_func("/feedbackd/fbd/feedback-theme/profiles", test_fbd_feedback_theme_profiles); + g_test_add_func("/feedbackd/fbd/feedback-theme/parse", test_fbd_feedback_theme_parse); + + return g_test_run(); +} diff --git a/tests/test-lfb-event.c b/tests/test-lfb-event.c new file mode 100644 index 0000000..04aba0a --- /dev/null +++ b/tests/test-lfb-event.c @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#include "libfeedback.h" +#include + +/* Property set/get */ +static void +test_lfb_event_props (void) +{ + g_autoptr(LfbEvent) event = NULL; + g_autofree gchar *evname = NULL; + gint timeout; + + g_assert_true (lfb_init (TEST_APP_ID, NULL)); + + event = lfb_event_new ("window-close"); + g_assert_true (LFB_IS_EVENT (event)); + + g_object_get (event, "event", &evname, NULL); + g_assert_cmpstr (evname, ==, "window-close"); + + g_object_get (event, "timeout", &timeout, NULL); + g_assert_cmpint (timeout, ==, -1); + + g_assert_cmpint (lfb_event_get_end_reason (event), ==, LFB_EVENT_END_REASON_NATURAL); + g_assert_cmpint (lfb_event_get_state (event), ==, LFB_EVENT_STATE_NONE); + + lfb_uninit (); +} + +/* Test failure paths when no feedback daemon is running */ +static void +test_lfb_event_trigger (void) +{ + g_autoptr(LfbEvent) event = NULL; + g_autofree gchar *evname = NULL; + g_autoptr (GError) err = NULL; + + g_assert_true (lfb_init (TEST_APP_ID, NULL)); + + event = lfb_event_new ("window-close"); + g_assert_false (lfb_event_trigger_feedback (event, &err)); + g_assert_nonnull (err); + g_clear_error (&err); + + g_assert_false (lfb_event_end_feedback (event, &err)); + g_assert_nonnull (err); + g_clear_error (&err); + + lfb_uninit (); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_autoptr(GTestDBus) bus = g_test_dbus_new (0); + g_test_dbus_up (bus); + + g_test_add_func("/feedbackd/libfeedback/lfb-event/props", test_lfb_event_props); + g_test_add_func("/feedbackd/libfeedback/lfb-event/trigger", test_lfb_event_trigger); + + return g_test_run(); +} diff --git a/tests/test-lfb-integration.c b/tests/test-lfb-integration.c new file mode 100644 index 0000000..53a688b --- /dev/null +++ b/tests/test-lfb-integration.c @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2020 Purism SPC + * SPDX-License-Identifier: GPL-3.0+ + * Author: Guido Günther + */ + +#include "libfeedback.h" +#include + +typedef struct { + GTestDBus *dbus; + +} TestFixture; + +GMainLoop *mainloop; + +static void +fixture_setup (TestFixture *fixture, gconstpointer unused) +{ + g_autoptr (GError) err = NULL; + gchar *relative, *servicesdir; + gint success; + + fixture->dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + relative = g_test_build_filename (G_TEST_BUILT, "services", NULL); + servicesdir = g_canonicalize_filename (relative, NULL); + g_free (relative); + + g_test_dbus_add_service_dir (fixture->dbus, servicesdir); + g_free (servicesdir); + g_setenv ("FEEDBACK_THEME", TEST_DATA_DIR "/test.json", TRUE); + g_setenv ("GSETTINGS_BACKEND", "memory", TRUE); + g_test_dbus_up (fixture->dbus); + + g_assert_null (mainloop); + mainloop = g_main_loop_new (NULL, FALSE); + + success = lfb_init (TEST_APP_ID, &err); + g_assert_no_error (err); + g_assert_true (success); +} + +static void +fixture_teardown (TestFixture *fixture, gconstpointer unused) +{ + g_clear_pointer (&mainloop, g_main_loop_unref); + lfb_uninit (); + + g_test_dbus_down (fixture->dbus); + g_object_unref (fixture->dbus); +} + +static void +on_feedback_ended (LfbEvent *event, LfbEvent **cmp) +{ + g_assert_true (LFB_IS_EVENT (event)); + g_assert_null (*cmp); + + g_debug ("Feedback ended for %s", lfb_event_get_event (event)); + g_assert_cmpint (lfb_event_get_end_reason (event), ==, LFB_EVENT_END_REASON_EXPLICIT); + + /* "Return" event */ + *cmp = event; + g_main_loop_quit (mainloop); +} + +static void +test_lfb_integration_event_sync (void) +{ + g_autoptr(LfbEvent) event0 = NULL; + g_autoptr(LfbEvent) event1 = NULL; + g_autoptr(LfbEvent) event10 = NULL; + g_autofree gchar *evname = NULL; + g_autoptr (GError) err = NULL; + LfbEvent *cmp = NULL; + gboolean success; + + event0 = lfb_event_new ("test-dummy-0"); + success = lfb_event_trigger_feedback (event0, &err); + g_assert_no_error (err); + g_assert_true (success); + + event10 = lfb_event_new ("test-dummy-10"); + g_signal_connect (event10, "feedback-ended", (GCallback)on_feedback_ended, &cmp); + success = lfb_event_trigger_feedback (event10, &err); + g_assert_no_error (err); + g_assert_true (success); + + success = lfb_event_end_feedback (event10, &err); + g_assert_no_error (err); + g_assert_true (success); + + g_main_loop_run (mainloop); + + /* If the signal fired cmp will match event */ + g_assert_true (event10 == cmp); + g_assert_cmpint (lfb_event_get_state (event10), ==, LFB_EVENT_STATE_ENDED); + g_assert_cmpint (lfb_event_get_end_reason (event10), ==, LFB_EVENT_END_REASON_EXPLICIT); +} + +static void +on_event_triggered (LfbEvent *event, + GAsyncResult *res, + LfbEvent **cmp) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_assert_true (LFB_IS_EVENT (event)); + g_assert_null (*cmp); + + g_debug ("%s", __func__); + success = lfb_event_trigger_feedback_finish (event, res, &err); + g_assert_no_error (err); + g_assert_true (success); + + /* "Return" event */ + *cmp = event; + g_main_loop_quit (mainloop); +} + +static void +on_event_ended (LfbEvent *event, + GAsyncResult *res, + LfbEvent **cmp) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_debug ("%s", __func__); + g_assert_true (LFB_IS_EVENT (event)); + g_assert_null (*cmp); + + success = lfb_event_end_feedback_finish (event, res, &err); + g_assert_no_error (err); + g_assert_true (success); + + /* "Return" event */ + *cmp = event; + g_main_loop_quit (mainloop); +} + +static void +test_lfb_integration_event_async (void) +{ + g_autoptr(LfbEvent) event0 = NULL; + g_autoptr(LfbEvent) event10 = NULL; + g_autofree gchar *evname = NULL; + g_autoptr (GError) err = NULL; + LfbEvent *cmp = NULL; + gboolean success; + + event0 = lfb_event_new ("test-dummy-0"); + lfb_event_trigger_feedback_async (event0, + NULL, + (GAsyncReadyCallback)on_event_triggered, + &cmp); + g_main_loop_run (mainloop); + /* The async finish callback saw the right event */ + g_assert_true (event0 == cmp); + cmp = NULL; + + event10 = lfb_event_new ("test-dummy-10"); + success = lfb_event_trigger_feedback (event10, &err); + g_assert_no_error (err); + g_assert_true (success); + + lfb_event_end_feedback_async (event10, + NULL, + (GAsyncReadyCallback)on_event_ended, + &cmp); + + g_main_loop_run (mainloop); + /* The async finish callback saw the right event */ + g_assert_true (event10 == cmp); + g_assert_cmpint (lfb_event_get_state (event10), ==, LFB_EVENT_STATE_ENDED); +} + +static void +on_profile_changed (LfbGdbusFeedback *proxy, GParamSpec *psepc, const gchar **profile) +{ + g_assert_null (*profile); + *profile = lfb_get_feedback_profile(); + g_debug("Set feedback profile to: '%s'", *profile); + + g_main_loop_quit (mainloop); +} + +static void +test_lfb_integration_profile (void) +{ + g_autoptr (GError) err = NULL; + LfbGdbusFeedback *proxy; + gchar *cmp = NULL; + + g_assert_cmpstr (lfb_get_feedback_profile (), ==, "full"); + proxy = lfb_get_proxy (); + g_assert_nonnull (proxy); + + lfb_set_feedback_profile ("quiet"); + g_signal_connect (proxy, "notify::profile", (GCallback)on_profile_changed, &cmp); + g_main_loop_run (mainloop); + g_assert_cmpstr (lfb_get_feedback_profile (), ==, "quiet"); + g_assert_cmpstr (cmp, ==, "quiet"); +} + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add("/feedbackd/lfb-integration/event_sync", TestFixture, NULL, + (gpointer)fixture_setup, + (gpointer)test_lfb_integration_event_sync, + (gpointer)fixture_teardown); + + g_test_add("/feedbackd/lfb-integration/event_async", TestFixture, NULL, + (gpointer)fixture_setup, + (gpointer)test_lfb_integration_event_async, + (gpointer)fixture_teardown); + + g_test_add("/feedbackd/lfb-integration/profile", TestFixture, NULL, + (gpointer)fixture_setup, + (gpointer)test_lfb_integration_profile, + (gpointer)fixture_teardown); + + return g_test_run(); +}