An Android music player that treats folders as playlists
Find a file
ika 2466e889e1 Search: tap-to-insert when playing, search icon in track screens
When a playlist is playing, tapping a search result shows a dropdown:
"Play now" (insert + seek to track immediately) or "Play next" (insert
after current track). Without an active playlist, tap behaviour is
unchanged. Search icon added to the TopAppBar of all TracksScreen
instances (playlist and Now Playing queue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 20:01:38 +02:00
app Search: tap-to-insert when playing, search icon in track screens 2026-04-04 20:01:38 +02:00
gradle Add playlist playback via Media3 ExoPlayer 2026-03-28 14:36:09 +01:00
.gitignore Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00
build.gradle.kts Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00
CLAUDE.md Replace SAF scanning with MediaStore.Audio for faster playlist loading 2026-04-04 18:05:28 +02:00
fdroid.md doc 2026-03-30 15:01:43 +02:00
gradle.properties Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00
gradlew Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00
LICENSE Fix license year to 2026 2026-03-29 14:30:40 +02:00
PLAN.md Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00
README.md Replace SAF scanning with MediaStore.Audio for faster playlist loading 2026-04-04 18:05:28 +02:00
settings.gradle.kts Initial implementation of Foldio playlist browser 2026-03-28 14:22:48 +01:00

Foldio

An Android music player that treats folders as playlists.

Features

  • Folder-based playlists — each subfolder of your chosen root becomes a playlist
  • Playback — play, pause, skip, shuffle; background playback as a foreground service
  • Multi-playlist shuffle — select multiple playlists and shuffle all tracks together
  • Album artwork — extracts embedded ID3 artwork; override per playlist with any track's art
  • Hide playlists — swipe left or right to hide a playlist; restore all from Settings
  • Empty playlists hidden — folders with no MP3 files are not shown
  • French localisation — switches automatically with system language

Build & Install

local.properties must contain the path to your Android SDK:

sdk.dir=/opt/android-sdk
# Build debug APK
./gradlew assembleDebug

# Build and install on connected device
./gradlew installDebug

Architecture

Stack: Kotlin · Jetpack Compose + Material3 · MVVM (ViewModel + StateFlow) · Navigation Compose · Jetpack DataStore · SAF (Storage Access Framework) · Media3 ExoPlayer · Coil

Data flow

SAF (ContentResolver.query via DocumentsContract)
  → PlaylistRepositoryImpl
  → PlaylistsViewModel  (activity-scoped, shared across screens)
  → PlaylistsScreen / TracksScreen

PlaylistsViewModel is hoisted to nav-graph scope so both screens share the same instance. PlayerViewModel is also activity-scoped.

File access

All file access goes through SAF — no READ_EXTERNAL_STORAGE permission is declared. The user picks a root folder via OPEN_DOCUMENT_TREE; the URI is persisted with takePersistableUriPermission. Directory listing uses ContentResolver.query with DocumentsContract (not DocumentFile.listFiles(), which is too slow).

Playback

PlaybackService (MediaSessionService) owns the ExoPlayer instance and runs as a foreground service. PlayerViewModel connects to it via MediaController (async, built in init). The queue is managed entirely through the controller; currentTrackUris in PlayerViewModel mirrors it so shuffle can operate without reading back from the controller.

Caching

  • ArtworkUtils.ktLruCache<String, Bitmap> (1/8 heap) + disk cache at cacheDir/artwork/<sha256>.png
  • MetadataUtils.ktLruCache<String, TrackMetadata> (500 entries) for ID3 tags

Both are keyed by URI string. Always checked before calling MediaMetadataRetriever.

Persistence

DataStore Contents
SettingsDataStore Root folder URI
ArtworkDataStore Per-playlist artwork overrides ("<playlistId>|<trackUri>")
HiddenPlaylistsDataStore Set of hidden playlist IDs

Navigation

playlists  →  tracks/{playlistId}  →  (back)
playlists  →  queue
playlists  →  settings  →  (back)

Playlist IDs are URL-encoded before being passed as nav arguments (SAF document IDs contain slashes).

Project Structure

app/src/main/java/com/foldio/android/
├── di/                 AppModule (singleton dependencies)
├── data/
│   ├── model/          Playlist, ArtworkSource
│   ├── repository/     PlaylistRepository + Impl
│   └── datastore/      Settings, Artwork, HiddenPlaylists
├── ui/
│   ├── navigation/     AppNavigation
│   ├── playlists/      PlaylistsScreen, PlaylistsViewModel
│   ├── tracks/         TracksScreen
│   ├── player/         PlaybackService, PlayerViewModel, NowPlayingBar
│   ├── settings/       SettingsScreen, SettingsViewModel
│   └── theme/
└── util/               ArtworkUtils, MetadataUtils, UriUtils

Requirements

  • Android 8.0+ (API 26)
  • Android SDK (compileSdk 35)