Skip to content

feat(pebble): Add full Pebble smartwatch integration (library browse + playback)#1597

Draft
tannermeade wants to merge 4 commits into
finamp-app:redesignfrom
tannermeade:redesign-pebble-support
Draft

feat(pebble): Add full Pebble smartwatch integration (library browse + playback)#1597
tannermeade wants to merge 4 commits into
finamp-app:redesignfrom
tannermeade:redesign-pebble-support

Conversation

@tannermeade

@tannermeade tannermeade commented Apr 13, 2026

Copy link
Copy Markdown

What does this PR do?

Adds complete support for the Pebble smartwatch to control Finamp from your wrist.

You can now:

  • Browse your library directly on the Pebble:
    • Albums
    • Playlists
    • Artists
    • Genres
  • “Load More…” pagination works for every list type (lazy-loaded, memory-safe)
  • Tap an item → opens a Songs screen with the parent name as header
    • First row = “Play [Item Name]” (replaces queue)
    • Remaining rows = individual tracks
  • Tap a track → adds it to “Next Up” and immediately plays it
  • All existing controls (Now Playing, Pause/Play, Next, Previous) continue to work
  • Rich Now Playing updates are sent continuously with accurate timeline

The implementation is stateless on the Dart side and follows the exact patterns used in the redesign branch (getItems, genreFilter, QueueItemSource.rawId, addNext, etc.).

Files changed

New / modified:

  • lib/services/pebble_service.dart ← main Dart integration (all logic)
  • android/app/src/main/kotlin/com/unicornsonlsd/finamp/PebbleHandler.kt ← messaging between pebble/finamp
  • android/app/src/main/kotlin/com/unicornsonlsd/finamp/PebbleListenerService.kt ← intent/background communication between pebble/finamp

How to test

  1. Build/install the Pebble app (pebble build && pebble install).
  1. Install the updated Finamp APK (redesign branch + this PR).
  2. Open the Pebble app → you should see the new main menu with 4 browse options.
  3. Test each list type:
    • Browse → tap item → songs screen appears
    • “Play [Name]” at the top
    • Tap any song → it should immediately start playing
    • “Load More Songs…” should work on large albums/playlists
  4. Verify Now Playing updates and transport controls still work.

Notes for reviewers

  • All new commands (CMD_OPEN_SONGS, CMD_LOAD_MORE_SONGS, CMD_PLAY_ALL, CMD_PLAY_SONG) are clearly defined and stateless.

Ready for review and merge into the redesign branch.

@Chaphasilor

Copy link
Copy Markdown
Member

Hi @tannermeade, thanks for the PR!

Could you please record a video and add some screenshots of how the app looks and works on the pebble?

Also, from a quick glance I saw you using a switch block that matches integers representing various commands. Could you please define those via an enum?

@HeroBrine1st HeroBrine1st left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty interesting. I also like that the changes are isolated.

Outside of structured concurrency violation and UX issue it should be okay I think

channel.setMethodCallHandler(this)

// === Grant permission for Core Pebble app to talk to us ===
CoroutineScope(Dispatchers.Main).launch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not GlobalScope? This is essentially GlobalScope which is delicate API, because both undermine structured concurrency

runBlocking is a viable alternative, too, without such problems but yeah, blocking

import android.content.Intent
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall // ← This was missing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not gatekeeping but I'd like those comments to be deleted because this is a simple agent communication with developer


Log.d("🪨", "Pebble command received → cmd: $cmd, index: $index, text: $receivedText")

// Fixed: safe null check (no more backing-field error)

@HeroBrine1st HeroBrine1st Apr 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially this. This can actually be a worthy comment because e.g. to me it's not obvious why this can be null (note I'm reviewing without IDE) so this can explain why such late-binding happens at all

Comment on lines +333 to +334
// Give the background task a moment to update the queue before skipping
await Future.delayed(const Duration(milliseconds: 150));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like race condition in QueueService? It's possible (I even found one in the past), I'm probably gonna look there

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have glanced into queue_service.dart file and found that it suspends until _audioHandler.insertFinampQueueItems is complete (it is awaited and everything in it is awaited too), so there should be no race condition requiring this delay. The same audioHandler is used below (skipToNext), so this should not be an issue and this delayed is then not needed.

Was there an actual issue? If so, it's most probably an upstream issue in just-audio

final track = await jellyfinApiHelper.getItemById(BaseItemId(songId));
if (track == null) return;

await queueService.addNext(items: [track]);

@HeroBrine1st HeroBrine1st Apr 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this function is called in response to CMD_PLAY_SONG, but in Finamp itself a similar action replaces the queue with either one track only (e.g. from tracks page if there's no mix) or with its queue (an album, playlist or a mix). Is this probably something Pebble UX related that it adds a track to current queue?

Comment on lines +338 to +342
if (state.playing) {
await audioHandler.skipToNext();
} else {
await audioHandler.play();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and looks like this logic is flawed. As far as I understand from just-audio reference, playing is true if player is actually playing. In this case, when player is paused skipToNext is not called and current track is played instead.

However, if I'm wrong and playing means something else (e.g. if it is true when paused too), the first branch indeed skips to just added track yet doesn't unpause.

I can't fully understand this even though I quickly skimmed into reference, so take this with a grain of salt especially considering that this response was completely different until I realized the just-audio method I referenced is different to one I read, so I rewrote this, although "if I'm wrong" part is based on the rewritten text.

@Chaphasilor

Copy link
Copy Markdown
Member

@tannermeade are you still around? :)

@Chaphasilor Chaphasilor added the awaiting response / stale Issue is possibly outdated or waiting for user feedback label Jul 2, 2026
@tannermeade

Copy link
Copy Markdown
Author

@tannermeade are you still around? :)

Ya, practically speaking I think some more changes need to be made to make this feature reliable and useful enough for me. Those changes will be a little more involved. If this PR needs to be closed for cleanliness of the repo feel free to close it and I'll reopen or make a new PR whenever I get around to making the changes that need to happen for the feature to work well enough that the phone doesn't need to be fiddled with ever.

@Chaphasilor

Copy link
Copy Markdown
Member

@tannermeade no worries, I'll just convert it to a draft PR, and you can make any changes you feel are necessary. Once you feel it's ready to be merged, just ping one of us. And let us know if you have any questions about the code or how best do achieve something specific!

@Chaphasilor
Chaphasilor marked this pull request as draft July 4, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting response / stale Issue is possibly outdated or waiting for user feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants