Restrict the brightness sink to one app, picked by name not typed
Confirmed the brightness command applies globally, not per-LED
(serverinfo showed exactly one adjustment object, "id": "default",
covering the whole string), so no LED-count configuration is needed
for this at all -- that question resolved itself once the mechanism
was actually inspected instead of assumed.
For "only react while Spotify is running": only one app can be in the
foreground on webOS at a time, so a "capture the current app" button
in this app's own UI can never work -- pressing it means this app is
foreground, not Spotify. The only workable UI is picking a target from
every *installed* app by name, regardless of what's currently running.
That needed a new native capability this service never had: calling
OUT to another Luna service, not just being called. Two additions:
foreground_app.c subscribes once, at startup, to
com.webos.applicationManager/getForegroundAppInfo
and keeps a thread-safe cache the audio thread can
read without a blocking Luna call
service.c new listApps method, bridging to
com.webos.applicationManager/listApps so the
frontend never has to call another service
directly -- same rule as everywhere else here
Until the subscription has delivered at least one reply, a restricted
sink treats the target app as inactive, not active -- reacting to
audio when the user explicitly restricted it to one app would be the
wrong failure mode. Verified end to end on the host: engine_smoke.c
opens the sink with a restriction set, confirms it reports itself
correctly inactive against the stub Luna bus (which always "fails" to
call out, exactly like a real host with no bus).
Needed real, linkable stub bodies for LSCall/LSCallOneReply/
LSCallCancel/LSMessageGetPayload/LSErrorInit/LSErrorFree
(test/stubs/luna-service2/lunaservice_stub.c) since foreground_app.c
is the first source file here that's actually linked into a host test
binary rather than only syntax-checked -- service.c/main.c's existing
stub declarations were never called, only compiled against. Confirmed
those really are the correct symbol names by cross-compiling clean
against the real webOS SDK's actual libluna-service2, not just the
stub.
Bumped to 1.0.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e9f6c87d27
commit
d3e4cb6410
+15
-3
@@ -204,12 +204,22 @@ static void test_status(engine_t* e)
|
||||
check(json_len(json_get(levels, "bands")) == DSP_BANDS, "all bands reported");
|
||||
|
||||
const json_value_t* sinks = json_get(v, "sinks");
|
||||
check(json_len(sinks) == 2, "two sinks reported");
|
||||
check(json_len(sinks) == 3, "three sinks reported");
|
||||
for (size_t i = 0; i < json_len(sinks); i++) {
|
||||
const json_value_t* s = json_at(sinks, i);
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "sink '%s' started cleanly", json_str(s, "id", "?"));
|
||||
check(json_bool(s, "ok", false), label);
|
||||
|
||||
if (strcmp(json_str(s, "id", ""), "hyperhdrAdjust") == 0) {
|
||||
// A host has no Luna bus (see lunaservice_stub.c), so the
|
||||
// sink can never confirm the restricted app is foreground.
|
||||
// The correct failure mode is inactive, not "assume yes".
|
||||
check(strcmp(json_str(s, "restrictToApp", ""), "some.other.app") == 0,
|
||||
"restriction target reported back");
|
||||
check(json_bool(s, "restrictedAppActive", true) == false,
|
||||
"restricted app correctly reported as not active (fail-closed)");
|
||||
}
|
||||
}
|
||||
json_free(v);
|
||||
}
|
||||
@@ -224,9 +234,11 @@ int main(void)
|
||||
char cfg_text[512];
|
||||
snprintf(cfg_text, sizeof(cfg_text),
|
||||
"{\"capture\":{\"backend\":\"tone\",\"rate\":48000,\"channels\":2},"
|
||||
"\"sinks\":[\"tcp\",\"http\"],"
|
||||
"\"sinks\":[\"tcp\",\"http\",\"hyperhdrAdjust\"],"
|
||||
"\"tcp\":{\"port\":%d},"
|
||||
"\"http\":{\"port\":%d}}",
|
||||
"\"http\":{\"port\":%d},"
|
||||
"\"hyperhdrAdjust\":{\"host\":\"127.0.0.1\",\"port\":19444,"
|
||||
"\"restrictToApp\":\"some.other.app\"}}",
|
||||
TCP_PORT, HTTP_PORT);
|
||||
|
||||
json_value_t* cfg = json_parse(cfg_text);
|
||||
|
||||
+4
-2
@@ -13,12 +13,14 @@ export PYTHONDONTWRITEBYTECODE=1
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
CFLAGS=(-std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -Inative/src -O1 -g)
|
||||
CFLAGS=(-std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -Inative/src -Itest/stubs -O1 -g)
|
||||
|
||||
SOURCES=(
|
||||
native/src/engine.c
|
||||
native/src/config.c
|
||||
native/src/dsp.c
|
||||
native/src/foreground_app.c
|
||||
test/stubs/luna-service2/lunaservice_stub.c
|
||||
native/src/common/log.c
|
||||
native/src/common/json.c
|
||||
native/src/common/ringbuf.c
|
||||
@@ -41,7 +43,7 @@ SOURCES=(
|
||||
|
||||
echo "== Syntax-checking the webOS-only sources against stub headers"
|
||||
for f in native/src/service.c native/src/main.c; do
|
||||
"$CC" "${CFLAGS[@]}" -Itest/stubs -fsyntax-only "$f"
|
||||
"$CC" "${CFLAGS[@]}" -fsyntax-only "$f"
|
||||
echo " ok $f"
|
||||
done
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ typedef struct {
|
||||
} LSError;
|
||||
|
||||
typedef bool (*LSMethodFunction)(LSHandle* sh, LSMessage* msg, void* category_context);
|
||||
typedef bool (*LSFilterFunc)(LSHandle* sh, LSMessage* reply, void* ctx);
|
||||
typedef unsigned long LSMessageToken;
|
||||
|
||||
typedef enum {
|
||||
LUNA_METHOD_FLAGS_NONE = 0,
|
||||
@@ -58,6 +60,17 @@ bool LSGmainAttach(LSHandle* handle, GMainLoop* loop, LSError* error);
|
||||
const char* LSMessageGetPayload(LSMessage* message);
|
||||
bool LSMessageIsSubscription(LSMessage* message);
|
||||
bool LSMessageReply(LSHandle* sh, LSMessage* message, const char* reply, LSError* error);
|
||||
void LSMessageRef(LSMessage* message);
|
||||
void LSMessageUnref(LSMessage* message);
|
||||
|
||||
bool LSSubscriptionAdd(LSHandle* sh, const char* key, LSMessage* message, LSError* error);
|
||||
bool LSSubscriptionReply(LSHandle* sh, const char* key, const char* payload, LSError* error);
|
||||
|
||||
// Client-call API: this service acting as a caller of another service, not
|
||||
// just a callee. LSCall keeps calling `callback` for every reply (used for
|
||||
// subscribe:true); LSCallOneReply auto-cancels after the first one.
|
||||
bool LSCall(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||
void* ctx, LSMessageToken* ret_token, LSError* error);
|
||||
bool LSCallOneReply(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||
void* ctx, LSMessageToken* ret_token, LSError* error);
|
||||
bool LSCallCancel(LSHandle* sh, LSMessageToken token, LSError* error);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Linkable bodies for the handful of luna-service2 client-call functions
|
||||
// foreground_app.c calls. service.c/main.c only ever get -fsyntax-only'd, so
|
||||
// declarations alone are enough for them; foreground_app.c is linked into
|
||||
// real host test binaries (engine_smoke, rtp_send) via SOURCES[] in
|
||||
// run-tests.sh, so those symbols need bodies too, or the link fails.
|
||||
//
|
||||
// A host has no Luna bus, so "the call failed" is exactly the right
|
||||
// simulated behaviour -- every caller here already treats that as
|
||||
// "foreground app tracking unavailable" and degrades accordingly, which is
|
||||
// also genuinely exercised by the test suite (see the fail-closed check in
|
||||
// engine_smoke.c).
|
||||
#include "luna-service2/lunaservice.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void LSErrorInit(LSError* error)
|
||||
{
|
||||
memset(error, 0, sizeof(*error));
|
||||
}
|
||||
|
||||
void LSErrorFree(LSError* error)
|
||||
{
|
||||
(void)error;
|
||||
}
|
||||
|
||||
bool LSCall(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||
void* ctx, LSMessageToken* ret_token, LSError* error)
|
||||
{
|
||||
(void)sh;
|
||||
(void)uri;
|
||||
(void)payload;
|
||||
(void)callback;
|
||||
(void)ctx;
|
||||
(void)ret_token;
|
||||
if (error)
|
||||
error->message = (char*)"no Luna bus on this host";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LSCallOneReply(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||
void* ctx, LSMessageToken* ret_token, LSError* error)
|
||||
{
|
||||
return LSCall(sh, uri, payload, callback, ctx, ret_token, error);
|
||||
}
|
||||
|
||||
bool LSCallCancel(LSHandle* sh, LSMessageToken token, LSError* error)
|
||||
{
|
||||
(void)sh;
|
||||
(void)token;
|
||||
(void)error;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* LSMessageGetPayload(LSMessage* message)
|
||||
{
|
||||
(void)message;
|
||||
return NULL;
|
||||
}
|
||||
@@ -133,6 +133,16 @@ async function main() {
|
||||
check('hyperhdr card is first and marked',
|
||||
doc.querySelector('.sink-card .badge').textContent === 'Recommended');
|
||||
check('hyperhdr host field exists', !!doc.querySelector('[data-path="hyperhdr.host"]'));
|
||||
const restrictPicker = doc.querySelector('[data-path="hyperhdrAdjust.restrictToApp"]');
|
||||
check('restrict-to-app picker exists', !!restrictPicker);
|
||||
eq('restrict-to-app picker starts on Always active', restrictPicker.textContent, 'Always active');
|
||||
// Always active -> the first installed app alphabetically by title
|
||||
// ("Live TV", ahead of Netflix/Spotify/YouTube in the mock's list).
|
||||
click(restrictPicker);
|
||||
await wait(600);
|
||||
eq('picking an app reaches settings by id, not a typed value',
|
||||
window.App.state.settings.hyperhdrAdjust.restrictToApp, 'com.webos.app.livetv');
|
||||
eq('picker now shows the app name, not the id', restrictPicker.textContent, 'Live TV');
|
||||
check('backend choice exists', !!doc.querySelector('[data-path="capture.backend"]'));
|
||||
check('log level choice exists', !!doc.querySelector('[data-path="logLevel"]'));
|
||||
check('boot toggle exists', !!doc.querySelector('[data-path="autoStart"]'));
|
||||
|
||||
Reference in New Issue
Block a user