Skip to content

feat: implement configurable PTT button system - #6

Open
romdj wants to merge 38 commits into
mainfrom
feat/configurable-ptt-buttons
Open

romdj wants to merge 38 commits into
mainfrom
feat/configurable-ptt-buttons

Conversation

@romdj

@romdj romdj commented Jan 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Multi-button PTT support: Volume buttons, play/pause, on-screen, and platform-specific options
  • Voice assistant prevention: Intercept long-press to block Google Assistant (Android) with documented iOS Siri limitation
  • Settings UI: Complete configuration interface for PTT mode, button selection, and screen wake lock
  • Platform updates: Android SDK 36, Kotlin 2.1, Gradle 8.9; iOS/macOS CocoaPods configuration

Changes

Core PTT Features

  • Add PTTButton enum and PTTConfiguration class for multi-button support
  • Implement dispatchKeyEvent() on Android to intercept buttons before system handlers
  • Add VolumeButtonObserver on iOS using KVO for volume button capture
  • Extend PTTService with configuration management and wakelock integration

UI

  • Add interactive on-screen PTT button with tap/hold gesture support
  • Create SettingsScreen with mode selector, button selector, and platform tips

Documentation

  • Add 9 documentation files covering implementation, testing phases, and troubleshooting

Test plan

  • Deploy to physical Android device
  • Test volume button PTT activation
  • Verify Google Assistant does NOT activate on long-press
  • Test on-screen button with tap and hold modes
  • Verify settings changes apply in real-time
  • Deploy to physical iOS device and verify volume/headset buttons

romdj added 30 commits January 25, 2026 15:31
- Upgrade compileSdk and targetSdkVersion from 34 to 36
- Update Kotlin stdlib and plugin to 2.1.0
- Update Android Gradle Plugin to 8.7.0
- Update Gradle wrapper to 8.9
- Add Pods xcconfig includes for iOS and macOS builds
- Register wakelock_plus and package_info_plus plugins for macOS
- Add Podfiles for iOS and macOS dependency management
Add PTTButton enum and PTTConfiguration class for multi-button support:
- Volume buttons (device volume down/up)
- Play/Pause button (Bluetooth/wired headsets)
- On-Screen button (universal fallback)
- System PTT (iOS 16+ only)

Add wakelock_plus dependency for screen wake lock during rides.
…tion

Add comprehensive button handling in MainActivity.kt:
- Volume button interception via onKeyDown/onKeyUp
- Play/pause button handling via dispatchKeyEvent
- Long-press detection to prevent Google Assistant activation
- Dynamic button routing based on configuration
- Screen wake lock support via FLAG_KEEP_SCREEN_ON
- Toggle and Hold mode support with debounce

Add MODIFY_AUDIO_SETTINGS permission for audio session control.
Add comprehensive button handling in AppDelegate.swift:
- VolumeButtonObserver class using KVO for volume button capture
- Automatic volume reset to prevent actual volume changes
- MPRemoteCommandCenter for headset button handling
- Screen wake lock via isIdleTimerDisabled
- Dynamic button configuration via platform channel

Note: iOS cannot prevent Siri activation on long-press (system restriction)
Extend PTTService with configuration capabilities:
- PTTConfiguration class for mode, button, and screen lock settings
- Dynamic native platform synchronization via method channel
- WakeLock integration with wakelock_plus package
- Auto-switch to toggle mode for play/pause button
- Real-time configuration updates without restart
Add interactive on-screen PTT button to HomeScreen:
- Tap/hold gesture support based on PTT mode
- Visual feedback with glow effects
- Current button configuration display
- Dynamic instruction text

Create SettingsScreen with complete configuration UI:
- PTT mode selector (toggle/hold)
- Button type selector with platform filtering
- Screen lock toggle switch
- Platform-specific tips and limitation warnings
Add comprehensive PTT documentation:
- PTT_IMPLEMENTATION_SUMMARY.md: Complete feature overview and architecture
- TESTING.md: Voice assistant prevention testing guide
- PHASE1_VOLUME_BUTTON_TESTING.md: Volume button validation
- PHASE2_ONSCREEN_BUTTON_TESTING.md: On-screen button validation
- PHASE3_SETTINGS_UI_TESTING.md: Settings UI validation
- VOICE_ASSISTANT_FIX.md: Voice assistant prevention details
- QUICK_START.md: Quick setup instructions
- WIRELESS_DEBUG_SETUP.md: Wireless debugging setup
- docs/bluetooth-ptt-implementation.md: Bluetooth integration details
- Update .nvmrc from v22 to v24
- Update README.md with PTT documentation links and known limitations
- Add .claude/settings.json for Claude Code configuration
- Add connect-android.sh for wireless ADB debugging workflow
Implement WebSocket-based signaling server for WebRTC peer coordination:
- Hub for managing rooms and client connections
- Client handling with read/write pumps
- Message types for offers, answers, ICE candidates, and PTT events
- Health check and stats endpoints
- Docker build support
Add Flutter services for P2P voice communication:
- SignalingClient: WebSocket client for room/peer management
- WebRTCService: Peer connection and audio stream handling
- Dependencies: flutter_webrtc, web_socket_channel, uuid
Add CallScreen for P2P voice calls:
- Shows connected peers and their talking status
- PTT button integration with WebRTC mute/unmute
- Connection status indicator
- Join room dialog from home screen
Add infrastructure for running services:
- docker-compose.yaml for local development
- k3s/signaling-deployment.yaml for Kubernetes
- Prepared placeholders for Redis, Postgres, Kafka (Phase 2)
Document the signaling service and WebRTC integration:
- Updated architecture diagram with services structure
- MVP status and test matrix
- Quick start for running signaling server
- WebSocket message reference
- Connection example code
- Next steps for testing
Extract PTT media-button handling from MainActivity into a foreground
MediaSessionService (PttMediaSessionService + PttPlayer) so headset
play/pause events keep arriving when the activity is backgrounded or
the screen is off. KeyEvents flow back to the activity via PttEventBus
for translation into Flutter MethodChannel calls.

Bug fix: BT headset play/pause buttons emit ACTION_DOWN+ACTION_UP
back-to-back at release time, which in hold mode produced a brief
green flash that immediately reverted to red. handleKeyEventForPTT
now forces toggle semantics for media keycodes (HEADSETHOOK,
MEDIA_PLAY_PAUSE, MEDIA_PLAY, MEDIA_PAUSE) regardless of pttMode,
so volume buttons can stay in hold mode while play/pause taps
toggle cleanly.

Also fixes the native default pttButton ("volumeDown" -> "volume")
to match the Dart-side enum name and adds verbose KeyEvent logging
under the PTT tag.
Introduce RecorderService wrapping microphone capture (record pkg) and
local playback (audioplayers pkg) for the PTT loopback POC: press
records, release stops + plays the clip through the device speaker.
PTTService drives the recorder lifecycle from its state transitions
so every press/release path (system PTT, headset, on-screen, manual
trigger) flows through one place.

PTTService now pushes its full configuration to the native side at
construction so Dart defaults and native defaults can never drift
silently. Removes the setButton auto-switch that forced toggle mode
on play/pause selection: the Android handler now forces toggle for
media keycodes regardless of mode, so the user can keep hold mode
for volume buttons without breaking play/pause taps.

Adds the corresponding plugin registrant updates for linux, macos,
and windows targets generated by flutter pub get.
Add PTTSystemManager wrapping iOS 16+ PTChannelManager so the user can
opt into the system-level PushToTalk experience (works from the lock
screen, surfaces in Control Center). Flutter calls joinPTTChannel /
leavePTTChannel and the manager reports begin/end transmit back over
the existing PTT method channel.

AppDelegate now routes joinPTTChannel, leavePTTChannel,
beginSystemPTTTransmit and stopSystemPTTTransmit method calls to the
manager (gated on iOS 16+). Adds the Runner.entitlements file required
by the PushToTalk framework.
Covers the state machine and configuration paths that were silently
fragile before:

- initial config push to native at construction (the regression that
  let Dart and native defaults drift, breaking the first press)
- pttPressed / pttReleased state transitions driving the recorder
- idempotent state transitions don't double-fire the recorder
- setMode / setPreventScreenLock forward to native
- setButton(playPause) preserves mode (regression test for the
  removed auto-switch — native now forces toggle for play/pause
  keycodes regardless of mode)
- active recording is stopped when configuration changes
- dispose tears down the recorder
CLAUDE.md and the PTT implementation docs still described Bluetooth
headset button capture as the project's unsolved "major blocker" and
described an outdated architecture (dispatchKeyEvent, MediaSessionCompat,
MPRemoteCommandCenter-only). The current branch already implements a
working solution on both platforms: a foreground Media3
MediaSessionService on Android and Apple's PushToTalk framework on iOS.

- Rewrite .claude/CLAUDE.md to reflect the current architecture and
  status instead of the historical blocker framing
- Rewrite docs/bluetooth-ptt-implementation.md to document the actual
  PttMediaSessionService / PTTSystemManager implementation and the
  remaining AVRCP protocol constraints (not bugs)
- Update PTT_IMPLEMENTATION_SUMMARY.md's architecture section and
  future-enhancements list to match what's actually built
- Replace the ad-hoc "# Working Roadmap.md" scratch file with
  ROADMAP.md: current status, confirmed protocol limitations, and a
  prioritized next-steps list (headset volume-button PTT via
  VolumeProvider, wiring PTT state to WebRTC audio, defaulting iOS to
  systemPTT, config persistence)
Design for wiring PTT state to WebRTC mic mute-gating over a persistent peer
connection, a minimal club-BBB/7-group room model, and a LAN-first (STUN-only)
2-device test milestone. Establishes RideSession as the coordinator that
supersedes the local loopback recorder.
…a group picker

Extract the PTT->WebRTC sync out of CallScreen's build method into RideSession
(muted by default, unmute on PTT hold), fixing the every-rebuild side-effect and
wiring pre-existing-peer connection. Home screen now routes to the club BBB group
picker. Also wrap the home body in a scroll view to fix a pre-existing layout
overflow surfaced by the smoke test.
The Flutter tooling migrator added android.builtInKotlin=false and
android.newDsl=false when building against the current SDK. Commit them so
builds are reproducible across machines instead of re-migrating on each run.
Headset volume buttons never reached the app: with AVRCP absolute volume the
headset sends SET_ABSOLUTE_VOLUME straight to the audio system, so no KeyEvent
is ever generated and only the phone's physical volume keys worked.

Claim remote device volume on the media session (the Media3 equivalent of a
legacy VolumeProvider) so the framework delivers those steps to PttPlayer as
increase/decrease callbacks instead. Volume is pinned to the middle of the
range after each step, so the user's real media volume is never changed and
there is always headroom in both directions.

Remote volume is claimed only while the volume button is selected for PTT, via
a shared PttConfig, so users driving PTT from headset play/pause keep normal
system volume behaviour.

AVRCP only ever reports discrete steps, never a down/up pair, so headset volume
PTT is toggle-only by protocol. These arrive on a separate PttEventBus discrete
channel and share MainActivity's single debounced toggleRecording() transition,
keeping every input source indistinguishable downstream.
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

A local macOS build of the signaling server (Mach-O executable) had been committed.
Remove it from the index and ignore the build output and coverage profile so they
can't be committed again. Also apply gofmt -s to messages.go, the only unformatted
file, ahead of formatting becoming a CI gate.
The signaling service is the live backend for PTT but had no tests, so a CI job
for it would have been a hollow gate.

Unit tests drive the hub handlers directly: joiners receive the existing peer list
and existing peers are notified; switching and leaving rooms notifies the old room
and removes empty rooms; unregistering closes the send channel without double-close
panics; offers are forwarded only to a target in the same room (room isolation);
broadcasts honour exclusions; a full send buffer drops instead of blocking the hub.
A wire-format test pins the JSON field names the Flutter client depends on.

Integration tests (build tag "integration") run the real HTTP handlers over real
WebSocket connections: two riders in one group negotiate offer/answer/ICE with the
server stamping the sender, PTT start/end reaches the group but not the talker or
another group, disconnects notify the group, and PTT before joining or unknown
message types are rejected.
ALLOWED_ORIGINS is documented as a list, but getEnvList returned the whole
environment value as a single entry, so "https://a, https://b" became one bogus
origin. Split on commas, trim whitespace, and drop empty entries, falling back to
the default when nothing remains.

Found by the new config tests, which also cover defaults, environment overrides,
and fallback on malformed PORT/LOG_JSON values.
govulncheck reported GO-2026-6278 (weak PRNG for the WebSocket mask key) in
gorilla/websocket v1.5.1, reachable from the client read/write pumps and the
upgrade handler. Upgrade to the fixed v1.5.3; govulncheck now reports no
vulnerabilities reachable from our code.

Also move the Dockerfile builder from Go 1.21 to 1.27 so the shipped binary uses
the same standard library that CI builds, tests, and vulnerability-scans.
Make the mobile code pass the stricter checks the CI pipeline enforces
(dart format and flutter analyze --fatal-infos):
- add missing @OverRide annotations on SignalingChannel / VoiceTransport
  implementations
- replace deprecated Color.withOpacity with withValues(alpha:) and
  Switch.activeColor with activeThumbColor
- add const constructors where suggested
- format 7 files

No behaviour change; all 20 tests pass.
The existing workflows never produced a green run: mobile CI derived Flutter from
pubspec's lower bound (3.16 / Dart 3.2) and failed at pub get, the live signaling
service had no job, and triggers only covered main and the deleted dev branch.

Add one pipeline modelled on the tempsdarret.studio setup, sized to this repo:
1. Quality: dart format + flutter analyze --fatal-infos; go mod verify/tidy,
   gofmt, go vet
2. Build & Unit, in parallel: flutter tests with coverage; Android debug APK
   (compiles the Kotlin PTT layer); iOS no-codesign build (compiles the Swift
   PushToTalk layer); signaling build, race-enabled unit tests, container image
3. Integration: real WebSocket signaling flows
4. Security: CodeQL (Go and workflows), govulncheck, full-history gitleaks scan
5. CI Status: aggregate job that fails unless every gate succeeded, intended as
   the single required check on main

Toolchains are pinned (Flutter 3.47.1, Go 1.27.x, Java 17). Runs on main and
feat/fix/hotfix/chore/ci branches plus PRs into main, cancelling superseded runs.
The one gitleaks finding in history, a TURN credential example inside a comment
of the archived upstream webrtc demo, is reviewed and allowlisted.
Catch failures before they reach CI. Pre-commit runs only for the staged file
types: dart format and flutter analyze for Dart, gofmt and go vet for Go.
Pre-push runs the Flutter tests and the signaling unit and integration tests.
Lefthook was chosen over husky because the repo has no Node toolchain.
Add docs/ci-pipeline.md: stage diagram, what each job checks with the command to
reproduce it locally, toolchain pins and what to keep in sync, lefthook setup, what
is deliberately not automated (headset E2E, Dart/Kotlin CodeQL, legacy server), and
per-job troubleshooting.

Update CLAUDE.md with the signaling test commands, CI/hook conventions, and mark
the headset volume-button workaround as implemented rather than planned.
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants