- Kotlin 100%
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> |
||
|---|---|---|
| app | ||
| gradle | ||
| .gitignore | ||
| build.gradle.kts | ||
| CLAUDE.md | ||
| fdroid.md | ||
| gradle.properties | ||
| gradlew | ||
| LICENSE | ||
| PLAN.md | ||
| README.md | ||
| settings.gradle.kts | ||
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.kt—LruCache<String, Bitmap>(1/8 heap) + disk cache atcacheDir/artwork/<sha256>.pngMetadataUtils.kt—LruCache<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)