Quellcode durchsuchen

fix: keep Music equalizer quiet after Jellyfin pause

Jellyfin pauses immediately, but CarDash's own MediaSession snapshot can remain playing for one 900 ms system poll. The Music tile now ignores CarDash's package and stays quiet; the Home card also stays on the paused Jellyfin queue. Adds regression tests and bumps to 1.2.1+21.
Krasimir Kazakov vor 6 Tagen
Ursprung
Commit
3bbd65769c

+ 1 - 1
AGENTS.md

@@ -97,7 +97,7 @@ notifyListeners();
 - `tool/emulator.sh` starts a headless AVD with the **host GPU**; SwiftShader segfaulted on this machine's API 35 image. Use `CARDASH_GPU=swiftshader_indirect` only as a fallback. Use `adb exec-out screencap -p` for screenshots; there may be multiple `adb` binaries on PATH, so use the absolute one.
 - While CarDash is foreground, `MainActivity` consumes hardware volume keys and adjusts `STREAM_MUSIC` so they match the dashboard, unless `AudioManager.isVolumeFixed` is true; on fixed-volume car units the keys pass through to the vehicle amplifier and the on-screen slider is disabled with an explanation. `SystemBridge.setVolume` falls back to relative `adjustStreamVolume` steps when a unit ignores absolute `setStreamVolume`. Long-pressing **Media volume** toggles `SystemController.volumeDiagnostics`, which shows the raw `index/max`, fixed flag, all stream levels and the last write result; the native side logs the same data under `CarDashVolume`. Do not remove this path without providing another sync path; otherwise Android controls the ring stream when no media is playing.
 - `FLAG_KEEP_SCREEN_ON` is controlled by `DisplayController` from `display.keepScreenOn` (default on) through `SystemBridge`; do not hard-code it in `MainActivity` or move it to a wake lock.
-- The Music and Jellyfin equalizers are exclusive. The Jellyfin tile animates only while `JellyfinPlaybackController.playing`; the Music tile animates only when the system reports media playing and Jellyfin is not playing. Jellyfin title/progress come from the native playback controller even without notification access; other apps use the notification-listener component and active MediaSession. Never read or store notification content.
+- The Music and Jellyfin equalizers are exclusive. The Jellyfin tile animates only while `JellyfinPlaybackController.playing`; the Music tile animates only when the system reports media playing, Jellyfin is not playing, and the active system session is not CarDash itself (`system.media.packageName != appPackageName`). The package guard prevents the Music tile flashing while a Jellyfin pause propagates through the 900 ms system poll. Jellyfin title/progress come from the native playback controller even without notification access; other apps use the notification-listener component and active MediaSession. Never read or store notification content.
 - Night mode is a full-screen black overlay with a dim clock. Long-press only on empty background; tap anywhere to dismiss. It does not change brightness or add a wake lock.
 - Speed is shown only when `TelemetrySettingsController.speedEnabled` is on. OBD-II is preferred when connected and fresh; the GPS fallback is used when OBD speed is off or unavailable. Enabling speed with GPS as the source (no adapter selected or OBD speed off) asks for location permission; the normal resume path never prompts. GPS starts only with fine location and GPS, and clamps stationary noise below 2 km/h to 0.
 - Temperature is shown only when `TelemetrySettingsController.temperatureEnabled` is on. OBD ambient is preferred when connected and fresh; Open-Meteo/location weather is the fallback, and the source switch only appears while an OBD-II adapter is selected.

+ 7 - 6
README.md

@@ -19,7 +19,7 @@ the normal Android launcher mechanism. No overlays, no root, no kiosk setup.
 ## Download
 
 - [Gogs release page](https://git.krasi.net/kkazakov/car-interface-android/releases) — published releases
-- [cardash-android-1.2.0.apk](releases/cardash-android-1.2.0.apk) — universal Android release
+- [cardash-android-1.2.1.apk](releases/cardash-android-1.2.1.apk) — universal Android release
 - SHA-256 and build details: [releases/README.md](releases/README.md)
 
 Gogs limits release attachments to 32 MB, so the 59 MB universal APK is linked
@@ -134,8 +134,8 @@ live vehicle data.
 - **Media-aware Music and Jellyfin tiles** — the stylized music icon shows a
   small animated equalizer while another app is playing; the Jellyfin tile shows
   it only while CarDash's Jellyfin player is playing. The two are mutually
-  exclusive. Android exposes media-active state, not the playing package, to a
-  normal launcher; Jellyfin is known because CarDash owns that player. The tile
+  exclusive. CarDash ignores its own stale MediaSession when Jellyfin pauses, so
+  the Music tile does not flash while the system snapshot catches up. The tile
   labels are clipped to the rounded button width so long album names cannot
   widen the grid.
 - **Six accent colors** — Green, Blue, Amber, Rose, Violet or Teal, applied
@@ -392,7 +392,7 @@ flutter test
 ./tool/check.sh
 ```
 
-The 134 tests cover PIN hashing/persistence, settings persistence, theme/accent,
+The 139 tests cover PIN hashing/persistence, settings persistence, theme/accent,
 dark accent tinting, battery visibility, first-run state, the ten-minute
 onboarding hint, slot visibility, pinned apps, custom labels/icons, the
 16-button cap at 1280×800, 1280×720 and 1024×600, the dense shortcut grid, the
@@ -404,7 +404,8 @@ weather fallback, the update version/availability logic and the Settings update
 dot. Jellyfin coverage includes genre grouping, recent-played dedupe, the
 12.2 request contract, the dashboard controller and widget states, the genre
 picker and five-family cap, album page/track list/transport, playback queue and
-state mapping, the settings gating and the Home tile width clipping. Native
+state mapping, the settings gating, the Home tile width clipping and the
+equalizer's stale-session guard. Native
 package enumeration, the HOME role, volume, app launching and PIN flows were
 verified on API 35 and API 29 emulators. Location permission, reverse geocoding
 and the Open-Meteo fallback were verified live on the API 35 emulator.
@@ -414,7 +415,7 @@ real 12.2 server (Quick Connect, grouped genres, 6/5-card dashboard, album page,
 background playback, notification, media keys, exclusive equalizers, audio-focus
 handover, Random re-shuffle) and against a local 12.2 container. The OBD hardware
 path compiles and is covered by the parser/economy design, but no ELM327 adapter
-is available on this machine; socket behavior still needs a car test. The 1.2.0
+is available on this machine; socket behavior still needs a car test. The 1.2.1
 universal APK is built from this tree and published. The emulator has verified
 the in-app update path.
 

+ 11 - 4
docs/IMPLEMENTATION_PLAN.md

@@ -280,10 +280,12 @@ Android (Kotlin)
 7. **Screen-on battery use**: the flag applies only while the dashboard is
    foreground; it is not a background wake lock.
 8. **Media metadata**: the Music equalizer follows `AudioManager.isMusicActive`
-   and is suppressed while Jellyfin plays; Jellyfin title/progress come from its
-   own controller. Other apps' title/progress query the active MediaSession
-   through the notification-listener component, which is user-granted and scoped
-   to media metadata.
+   and is suppressed while Jellyfin plays and while the active system session is
+   CarDash's own package. That package guard prevents a flash while a Jellyfin
+   pause propagates through the 900 ms system poll. Jellyfin title/progress come
+   from its own controller. Other apps' title/progress query the active
+   MediaSession through the notification-listener component, which is
+   user-granted and scoped to media metadata.
 9. **Pinned customizations**: Rename affects only the Home label. Default
    restores the app icon; a chosen stylized icon is a bundled asset and cannot
    become unavailable.
@@ -324,6 +326,11 @@ Android (Kotlin)
 
 ## 8. Change log
 
+- 2026-10-31: CarDash 1.2.1+21 fixes the brief Music-tile equalizer flash after
+  pausing Jellyfin from the Home card. The Music equalizer now ignores
+  CarDash's own stale system MediaSession while a Jellyfin pause propagates, and
+  the Home card stays on the paused Jellyfin queue during that gap. 139 tests
+  pass; release `releases/cardash-android-1.2.1.apk`.
 - 2026-10-31: CarDash 1.2.0+20 adds the full optional Jellyfin music
   source. The earlier Settings-only Quick Connect work now feeds a Home tile
   after Music, a dashboard (Recently played / Recently added / Random, six cards

+ 8 - 2
docs/JELLYFIN_1.2.0_PLAN.md

@@ -1,7 +1,9 @@
 # CarDash Jellyfin 1.2.0 — Dashboard, Album Page and Playback
 
 Status: **released 2026-10-31** (Gogs tag `v1.2.0`, commit `2715eeb`).
-Target release: **CarDash 1.2.0+20**.
+Patch **1.2.1** fixes the brief Music-tile equalizer flash after pausing Jellyfin
+from Home.
+Target release: **CarDash 1.2.0+20**; patch **1.2.1+21**.
 Server used for API verification: **Jellyfin 12.2.0** at `https://jellyfin.drun.net/`
 (server `DarkTower`), plus a local 12.2.0 container with a generated
 6-album / 24-track / 24-raw-genre test library.
@@ -91,7 +93,8 @@ Post-plan refinements implemented on 2026-10-31:
 - The album cover is top-aligned with the track list and controls on the album
   page.
 - Music and Jellyfin equalizers are exclusive (the source that is actually
-  playing).
+  playing). CarDash's own stale MediaSession is ignored, so a Jellyfin pause
+  cannot briefly animate the Music tile.
 - The Jellyfin Home tile's artist/album subtitle is clipped to the rounded
   button width, so a long album title cannot widen the shortcut grid.
 - Selecting an album from the Random row re-rolls the Random row once playback
@@ -627,6 +630,9 @@ repair states. Add:
   - another app playing while Jellyfin is paused → equalizer on the Music tile
     only,
   - nothing playing → neither.
+- The Music tile also requires the active system media session to be an external
+  package. CarDash's own Jellyfin MediaSession is excluded, so pausing Jellyfin
+  cannot flash the Music equalizer while the system snapshot catches up.
 - `VolumeCard` state selection:
   - if Jellyfin is playing, or Jellyfin has a queue and no other music is
     playing: show the Jellyfin `MediaPlayback` and route play/pause to the

+ 2 - 0
lib/core/app_identity.dart

@@ -0,0 +1,2 @@
+/// The Android application ID. Keep in sync with the Gradle applicationId.
+const String appPackageName = 'com.wasp.cardash_launcher';

+ 1 - 1
lib/core/app_version.dart

@@ -1,2 +1,2 @@
 /// Keep this in sync with the `version` in pubspec.yaml.
-const String appVersion = '1.2.0';
+const String appVersion = '1.2.1';

+ 2 - 1
lib/data/models/jellyfin_playback.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/foundation.dart';
 
+import '../../core/app_identity.dart';
 import 'media_playback.dart';
 
 /// One track in the native Jellyfin playback queue.
@@ -190,7 +191,7 @@ class JellyfinPlaybackSnapshot {
       playing: playing,
       title: title,
       artist: artist,
-      packageName: 'com.wasp.cardash_launcher',
+      packageName: appPackageName,
       durationMs: durationMs,
       positionMs: positionMs,
     );

+ 27 - 0
lib/features/home/home_media_state.dart

@@ -0,0 +1,27 @@
+import '../../core/app_identity.dart';
+import '../../data/models/media_playback.dart';
+
+/// Whether the current system media session belongs to another app.
+///
+/// CarDash's own Jellyfin player also publishes a MediaSession. While Jellyfin
+/// pauses, the system snapshot can still report that session as playing for up
+/// to one poll (~900 ms), so it must not be treated as external music.
+bool isExternalSystemMedia(MediaPlayback media) {
+  return media.packageName != appPackageName;
+}
+
+/// Whether the Music tile should show its animated equalizer.
+///
+/// A Jellyfin pause updates the Jellyfin controller immediately, while the
+/// Android media snapshot can remain playing for one system poll. The Music
+/// tile must stay quiet during that gap instead of flashing until the next
+/// poll: if the active session is CarDash's own Jellyfin session, it is not
+/// another app's music.
+bool shouldShowMusicEqualizer({
+  required bool systemMusicPlaying,
+  required bool jellyfinPlaying,
+  required MediaPlayback systemMedia,
+}) {
+  if (!systemMusicPlaying || jellyfinPlaying) return false;
+  return isExternalSystemMedia(systemMedia);
+}

+ 11 - 3
lib/features/home/home_screen.dart

@@ -13,6 +13,7 @@ import '../jellyfin/jellyfin_dashboard_screen.dart';
 import '../jellyfin/jellyfin_playback_controller.dart';
 import '../settings/settings_screen.dart';
 import 'gps_speed_controller.dart';
+import 'home_media_state.dart';
 import 'launcher_controller.dart';
 import 'obd_controller.dart';
 import 'pinned_app_actions.dart';
@@ -94,7 +95,12 @@ class _HomeScreenState extends State<HomeScreen> {
         telemetrySettings.speedEnabled && !showObdSpeed && gps.hasSpeed;
 
     final jellyfinPlaying = jellyfinPlayback.playing;
-    final otherMusicPlaying = system.isMusicPlaying && !jellyfinPlaying;
+    final systemMediaIsExternal = isExternalSystemMedia(system.media);
+    final otherMusicPlaying = shouldShowMusicEqualizer(
+      systemMusicPlaying: system.isMusicPlaying,
+      jellyfinPlaying: jellyfinPlaying,
+      systemMedia: system.media,
+    );
 
     ShortcutSpec slotSpec(SlotType slot) {
       final assignment = launcher.assignmentFor(slot);
@@ -158,14 +164,16 @@ class _HomeScreenState extends State<HomeScreen> {
       ),
     ];
 
+    final otherSystemMusicPlaying =
+        system.isMusicPlaying && systemMediaIsExternal;
     final jellyfinActive =
         jellyfinPlayback.isActive &&
-        (jellyfinPlaying || !system.isMusicPlaying);
+        (jellyfinPlaying || !otherSystemMusicPlaying);
     final cardMedia = jellyfinActive
         ? jellyfinPlayback.mediaPlayback
         : system.media;
     final cardPlaying = jellyfinActive
-        ? jellyfinPlayback.playing
+        ? jellyfinPlaying
         : system.isMusicPlaying;
 
     return PopScope(

+ 1 - 1
pubspec.yaml

@@ -1,7 +1,7 @@
 name: cardash_launcher
 description: "CarDash — a calm, configurable Android car dashboard launcher."
 publish_to: "none"
-version: 1.2.0+20
+version: 1.2.1+21
 
 environment:
   sdk: ^3.13.7

+ 75 - 0
test/home_media_state_test.dart

@@ -0,0 +1,75 @@
+import 'package:cardash_launcher/core/app_identity.dart';
+import 'package:cardash_launcher/data/models/media_playback.dart';
+import 'package:cardash_launcher/features/home/home_media_state.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+const _carDashPlaying = MediaPlayback(
+  hasAccess: true,
+  hasSession: true,
+  playing: true,
+  packageName: appPackageName,
+);
+
+const _chromePlaying = MediaPlayback(
+  hasAccess: true,
+  hasSession: true,
+  playing: true,
+  packageName: 'com.android.chrome',
+);
+
+void main() {
+  group('shouldShowMusicEqualizer', () {
+    test(
+      'stays quiet while the stale session is CarDash after a Jellyfin pause',
+      () {
+        expect(
+          shouldShowMusicEqualizer(
+            systemMusicPlaying: true,
+            jellyfinPlaying: false,
+            systemMedia: _carDashPlaying,
+          ),
+          isFalse,
+        );
+      },
+    );
+
+    test('animates for another app', () {
+      expect(
+        shouldShowMusicEqualizer(
+          systemMusicPlaying: true,
+          jellyfinPlaying: false,
+          systemMedia: _chromePlaying,
+        ),
+        isTrue,
+      );
+    });
+
+    test('stays quiet while Jellyfin is playing', () {
+      expect(
+        shouldShowMusicEqualizer(
+          systemMusicPlaying: true,
+          jellyfinPlaying: true,
+          systemMedia: _carDashPlaying,
+        ),
+        isFalse,
+      );
+    });
+
+    test('falls back to system music with no media session', () {
+      expect(
+        shouldShowMusicEqualizer(
+          systemMusicPlaying: true,
+          jellyfinPlaying: false,
+          systemMedia: const MediaPlayback(),
+        ),
+        isTrue,
+      );
+    });
+  });
+
+  test('detects CarDash versus external media sessions', () {
+    expect(isExternalSystemMedia(_carDashPlaying), isFalse);
+    expect(isExternalSystemMedia(_chromePlaying), isTrue);
+    expect(isExternalSystemMedia(const MediaPlayback()), isTrue);
+  });
+}