Locking the screen killed audio on video playback even with the background-audio toggle armed. configureWebViewForMedia() ran from onCreate's delayed post AND from every onResume, re-calling addJavascriptInterface on each pass — five times in a 45s session. WebView binds injected objects at page-load time, so re-injecting over a live page leaves JS holding a stale proxy: the object stays truthy (passing the `bridge()?.` optional chain) while its methods vanish. Logcat showed 66 "WebView: Unknown object" errors and, in JS, "TypeError: setEnabled is not a function". So the toggle turned blue but never reached native. backgroundAudioEnabled stayed false, onStop never dispatched 'jellytau-background', the handoff never ran, and audio stopped the instant the screen locked. PiP and audio focus broke identically. - Register the bridges exactly once per WebView (identity-compared), and split the idempotent settings/chrome-client work into configureWebViewSettings() so it still runs on every resume. - Forward WebView console output to logcat as "JellyTauWeb". The frontend was previously invisible to adb, which is what made this bug so hard to place; keep it for the next boundary-spanning diagnosis. - setBackgroundAudioEnabled now reports whether native was actually reached instead of silently no-oping, so a dead bridge can never again masquerade as an armed toggle. Removing the re-injection revived a latent conflict it had been masking: the focus calls started working, and three AUDIOFOCUS_GAIN requesters inside one uid began fighting — MainActivity, ExoPlayer, and Chromium's own AudioFocusDelegate. The grant was followed ~45ms later by AUDIOFOCUS_LOSS, whose handler paused playback, so arming background audio (or just pressing play) paused the video in a loop. WebView already manages focus for <video>. Drop the redundant AndroidAudioFocus bridge, its listeners and its helpers entirely, and leave focus to whichever engine is actually rendering — consistent with the player-is-authoritative principle. Also drops the dead AndroidBackgroundAudio.isSupported() probe, unused since the button gate moved to platform(). TRACES: UR-040 | IR-025, DR-051 | UT-062
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { setBackgroundAudioEnabled } from "./backgroundAudio";
|
|
|
|
/**
|
|
* Bridge-reporting contract for the background-audio toggle.
|
|
*
|
|
* TRACES: UR-040 | IR-025, DR-051 | UT-062
|
|
*
|
|
* Regression guard for the "screen lock kills video audio" bug: MainActivity
|
|
* re-ran configureWebViewForMedia() on every onResume, re-calling
|
|
* addJavascriptInterface over a live page. WebView then served a stale proxy —
|
|
* `window.AndroidBackgroundAudio` stayed truthy but its methods were gone, so
|
|
* `setEnabled` threw `TypeError: e.setEnabled is not a function`.
|
|
*
|
|
* The old implementation swallowed that with `bridge()?.setEnabled(...)` inside
|
|
* a try/catch returning void, so the UI showed "armed" while native never got
|
|
* the flag — and onStop's `if (backgroundAudioEnabled)` guard never dispatched
|
|
* `jellytau-background`. Audio died the instant the screen locked.
|
|
*
|
|
* setBackgroundAudioEnabled must therefore REPORT whether native was actually
|
|
* reached, so a dead bridge can never masquerade as an armed toggle.
|
|
*/
|
|
describe("setBackgroundAudioEnabled", () => {
|
|
beforeEach(() => {
|
|
delete (window as unknown as Record<string, unknown>).AndroidBackgroundAudio;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("reports success when the bridge is present and the call lands", () => {
|
|
const setEnabled = vi.fn();
|
|
window.AndroidBackgroundAudio = { setEnabled };
|
|
|
|
expect(setBackgroundAudioEnabled(true)).toBe(true);
|
|
expect(setEnabled).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it("reports failure when the bridge object is absent entirely", () => {
|
|
expect(setBackgroundAudioEnabled(true)).toBe(false);
|
|
});
|
|
|
|
it("reports failure for a stale proxy whose methods are gone", () => {
|
|
// The exact shape of the bug: object present (so `?.` passes) but the
|
|
// method is missing after re-injection over a live page.
|
|
window.AndroidBackgroundAudio = {} as unknown as typeof window.AndroidBackgroundAudio;
|
|
|
|
expect(setBackgroundAudioEnabled(true)).toBe(false);
|
|
});
|
|
|
|
it("reports failure when the bridge method throws", () => {
|
|
window.AndroidBackgroundAudio = {
|
|
setEnabled: () => {
|
|
throw new TypeError("e.setEnabled is not a function");
|
|
},
|
|
};
|
|
|
|
expect(setBackgroundAudioEnabled(true)).toBe(false);
|
|
});
|
|
|
|
it("never throws out to the caller — the toggle must not break the player", () => {
|
|
window.AndroidBackgroundAudio = {
|
|
setEnabled: () => {
|
|
throw new Error("boom");
|
|
},
|
|
};
|
|
|
|
expect(() => setBackgroundAudioEnabled(false)).not.toThrow();
|
|
});
|
|
});
|