Android: no in-app volume control — leave volume to the system #64
Closed
opened 2026-08-18 06:00:07 +00:00 by logan
·
3 comments
No Branch/Tag Specified
main
fix/146-stub-etxtbsy
fix/175-wizard-follows-the-library
fix/231-setter-rollback
fix/197-duplicate-column-label
docs/225-fixtures-wav-tags
docs/220-skill-check-scope
test/217-fixture-names-in-queue-selection
fix/216-riff-parse-allocation
fix/170-queue-header-action-names
fix/210-nav-sheet-scroll-affordance
docs/50-readme-landing-page
feat/65-art-prefetch-ahead
feat/71-more-as-a-bottom-sheet
feat/54-native-touch-feel
feat/67-entity-links-into-menus
test/196-visual-tier-gates
fix/138-ui-test-storage-leak
fix/104-wav-tags-read
fix/207-sheet-scroll-affordance
fix/204-ui-visual-update-filter
pi-agent-backlog-automation
63-touch-model-phase-2
63-android-touch-model
186-touch-targets-settings
186-touch-targets-page-header
187-seek-bar-hit-area
189-190-explore-correctness
135-android-underrun-instrumentation
51-android-small-screens
fix/171-phone-queue-scrim
fix/137-touch-only-affordances
fix/154-nested-css-check
feat/58-mini-player-progress-line
fix/66-album-page-scrolls-as-one
60-context-menu-action-sheet
64-android-system-volume
59-slim-the-mini-player
55-queue-as-a-screen
feat/57-drop-the-android-top-bar
feat/62-jobs-as-a-notification
fix/53-seek-bar-never-moves
fix/159-android-task-app-id
fix/52-android-activity-recreation-restarts-the-process
fix/150-expand-button-under-the-art
feat/42-inline-volume-and-centred-transport
fix/156-queue-selection-fixture-order
fix/151-fuse-the-scroll-guard-and-the-write
fix/43-queue-panel-selection
fix/143-top-bar-fits-its-window
feat/27-jobs-into-settings
feat/25-configurable-sidebar-tabs
feat/6-global-back-forward
fix/72-active-view-broadcast
fix/69-page-header-action-overflow
fix/quick-wins-batch
fix/118-in-library-clear
fix/61-mini-player-plain-text
fix/68-hover-affordances-pointer
fix/119-dev-headless-port
fix/130-issue-claim-user
fix/131-codegen-check-scope
feat/28-autotag-match-on-album
feat/17-demote-version-selector
feat/38-ownership-visibility
ci/115-manual-release
feat/34-icon-language
feat/7-full-tracklist-toggle
fix/16-tagwriter-totals
fix/unclaim-ca-certs
fix/unclaim-shell
ci/unclaim-on-close
docs/closing-keyword
docs/retire-stale-planning-docs
docs/issue-driven-workflow
integration/small-fixes
fix/small-issue-batch
fix/queue-toggle-state
fix/drag-count-badge
fix/album-card-year
fix/album-tracklist-heading
fix/seek-bar-clock-width
fix/explore-art-scanner-requests
chore/workflow-guardrails
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.3.1
v0.3.0
v0.2.3
v0.2.2
v0.2.1
v0.2.0
v0.1.0
v0.0.1
v0.0.0
Labels
Clear labels
Area/Design
Area/Downloads
Area/Explore
Area/Library-UI
Area/Metadata
Area/Packaging
Area/Player
Area/Queue
Area/Settings
Area/Shell-Nav
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Platform/Android
Platform/Desktop
Breaking change that won't be backward compatible
Something is not working
Documentation changes
Improve existing functionality
New functionality
This is security issue
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Medium
3
The priority is medium
Priority
Low
4
The priority is low
Reviewed
Confirmed
1
Issue has been confirmed
Reviewed
Duplicate
2
This issue or pull request already exists
Reviewed
Invalid
3
Invalid issue
Reviewed
Won't Fix
3
This issue won't be fixed
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
In Progress
Somebody is actively working on this right now
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yonlu/yellowjacket#64
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Report
On Android, volume is the device's job. We do not need our own volume control; assume the user handles it with the hardware keys and keep our internal level at 100%.
Findings
volume-controlis part of the shared bottom bar and already stands down at narrow widths; this asks for it to be absent on Android entirely.OnDuckis Android-specific: the framework asks for attenuation when something short needs the output, andPlayer.SetDuckkeeps that as an offset on top of the user's level rather than writing through to the volume — it must not persist or emit a level the user did not choose, and it only fires below API 26. Pinning "our level" at 100% must leave that offset mechanism intact.Direction
Hide the control on Android, keep the internal level at 1.0, and make sure nothing writes a persisted volume from that platform. Verify ducking still attenuates and restores.
Checked while planning #59/#56, because this issue is in the same
pixels. It is not done, and its first Finding is now misleading —
recording that rather than acting on it.
"
volume-control… already stands down at narrow widths" is true ofone of the two copies. Measured at 424x439:
platform:
index.css's phone block carries.bottom-bar volume-control { display: none }, added when #42 movedthe control into the bar.
now-playing-viewrenders one and it is visible on a phone. Thatis deliberate today — the comment beside the rule says so in as many
words: "
.bottom-bar volume-control, not the one innow-playing-view: that view is the phone's transport and is where aslider does belong."
So what this issue asks for is exactly the part that has not happened,
and it directly contradicts a decision that was made on purpose and
written down. That is worth settling before anyone writes code.
The crux is platform versus width, and the frontend cannot currently
tell. Every stand-down rule in this app is keyed on a viewport width,
because that is a question a browser can answer and every tier here can
test. "Absent on Android" is a different predicate, and there is no
isAndroidsignal in the frontend at all. Three options, none free:now-playing-view. Cheapest, testable in every tier — but it alsotakes the slider away from a narrow desktop window, which nobody
asked for and which has no hardware keys guaranteed.
and #63 will want one — but it is a new cross-cutting fact and
nothing can test the Android branch except a device.
option 1 with the reasoning made explicit rather than incidental.
The other two halves of the Direction are untouched and are backend
work: pinning the internal level at 1.0 on that platform, and making
sure nothing persists a volume from it — without disturbing
Player.SetDuck, which the second Finding is right to flag.Not claiming this. #59/#56 deliberately do not touch the volume control,
so nothing here is blocked by them.
Taking this, branch
64-android-system-volume. Reordering argued on#73 first (it is a Phase 4 "independent" item and Phase 3 has #58 and
#51 outstanding): #172's Direction names this as the one pure-gain
vertical win on Now Playing, and #51 is the verification pass that
should run after it.
The approach, and the crux is that this cannot be a viewport
question.
The first Finding is misleading and my comment above says why: the bar's
copy stands down by width, and
now-playing-view's copy is visible ona phone deliberately. So the choice is what predicate replaces it.
I am not extending the width rule to
now-playing-view, because thebackend half of this issue makes that wrong on the platform it is about.
Pinning the internal level at 1.0 on Android means a slider that moves
and changes nothing — and an Android tablet at >=600px would get the
bottom bar's copy, i.e. an inert control on exactly the platform this
issue exists for.
library-status-indicatoralready settled thatshape: a control that cannot act is worse than none.
So the predicate is a backend fact, and it is named after the capability
rather than the platform: "does the system own the volume". One
build-tagged constant (
android), everything else untagged andtherefore testable on every platform — the split
mediacontrolsalready uses, with
androidpayload.go's reasoning for keeping thecontract out of a tagged file. The frontend asks the player, not the
viewport, so the answer is right at every width.
Three parts:
backend/player—SetVolume/ChangeVolume/mute are no-ops wherethe system owns the volume, the level sits at max, and the stored
volume round-trips untouched rather than being overwritten with a
level the user did not choose.
SetDuckis untouched: it re-appliesthe user's level through
setVolumeLocked, so pinning that level tomax leaves the offset working exactly as it did.
volume-style-store—which already answers "what presentation does the volume control
get" for both mount points, and "none" is a presentation.
volume-controlrenders nothing and takes no space, so both the barand Now Playing get it from one place.
Then re-measure #172 at 424x439 and put the before/after on it.
What I will not be able to verify: no device is attached, so the
Android branch of the tagged constant is unexercised — the policy
either side of it is tested in Go, and the frontend's "no volume" branch
is testable in the component tier through the Wails fake. Ducking on a
real audio focus change is device-only and I will say so rather than
imply otherwise.
PR #174, CI green on both engines (WebKit step confirmed run, not
skipped).
The design landed as claimed: the predicate is a backend answer named
after the capability (
SystemOwnsVolume), onlyplatformOwnsVolumeisbuild-tagged, and
volume-controlrenders nothing in both mount pointsat every width. The bar's phone rule in
index.cssstays, restated aswhat a stylesheet can answer — room — rather than carrying the platform
argument it could not express.
On the two Findings. The first is corrected in the code rather than
just in prose: what "already stands down at narrow widths" described is
now explicitly the room rule, and the platform rule is a separate
mechanism beside it. The second is honoured —
SetDuckis untouched,and
TestSystemVolumeStillDuckspins the property (attenuate, restore,and a refused
SetVolumenot disturbing the offset, which is the newway that could have broken). The third — the Up/Down bindings sharing
the code path — is why
ChangeVolumeis refused rather than onlySetVolume, and mute is refused with them, since with no controlrendered it is the one state on such a platform there would be nothing
to get out of.
Not verified, and no device was attached: the tagged constant is
compiled by nothing here (a source sweep covers deletion and an edited
value, which is not the same as a compiler), and ducking against a real
audio-focus change is device-only.