Conversation
…oncile Groundwork for replacing the device-wide MediaStore scan with user-added folders and files (additive, nothing wired yet): - StableIds.songId: 63-bit hash of provider authority + documentId. A persisted format (favorites/playlists key on it), pinned by golden values computed with an independent MD5 implementation. - isAudioDoc / joinRelPath / audioCandidateOf: audio filter (mime, then extension for generic types; dot-files skipped), root-relative folder paths, SAF row -> candidate mapping. - SourceReconciler: incremental diff where a stored row may only be deleted when its source was enumerated completely, so an unavailable or partially listed source can never wipe the library. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One DocumentsContract child query per directory (no DocumentFile), an explicit stack with a visited set, cancellation checked per directory and row. Reports WALK status (complete / incomplete / unavailable) so a scan can tell "file is gone" from "provider or grant is gone". Not wired yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DB v3: new `sources` table (folder = TREE, single song = FILE) and songs now carry a `sourceId` FK with ON DELETE CASCADE; MediaStore columns are gone and `Song.id` is the stable SAF hash. MIGRATION_2_3 is a clean start (old songs dropped, playlist items / favorites / history cleared, playlist names kept), with DDL copied from the Room-generated code. Verified on a phone against a real v2 DB: user_version 3, foreign_key_check empty, schema identical to a fresh install. SafLibraryScanner replaces the MediaStore scanner: single-flight, walks every source that still has a read grant, source-scoped reconcile (only a COMPLETE source can delete), first-seen dateAdded, ignore-short applied after extraction. RoomSourceRepository takes/releases the persistable grant and cancels a running scan first; Add/Remove use cases rescan afterwards. Nothing scans automatically any more: onAppStarted / onForegrounded are gone. Removes the MediaStore pipeline (scanner, data source, candidate mapping, old Reconciler) and the dead SongDao/SongMapper helpers. PermissionDenied now means "lost access to this file's folder". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two system pickers (folder tree, multiple audio files) are created once in the app shell and exposed to screens through LocalMusicActions; a pick takes the grant and scans right away. Settings gets a "Music sources" group (a row per folder with song count and remove, one aggregate "Added songs" row, Add folder / Add songs, and a hint about the folders Android refuses). A folder whose access was lost stays listed and reopens the picker there. The empty state on Home and every Library tab now offers both actions. Also: "Rescan library" becomes "Refresh library"; Home no longer flashes the empty state before Room's first emission; playlist counts only count items whose song is still in the library; Settings stats refresh when a scan completes; sources are removed in one batch with a single rescan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The library no longer needs READ_MEDIA_AUDIO / READ_EXTERNAL_STORAGE, so the permission gates, manager, status model, the Settings "Audio access" row, the manifest permissions, the perm_* strings, the permissionAsked pref and the CheckingPermission / PermissionRequired scan states go. Home and Library render their content directly. FOREGROUND_SERVICE* and POST_NOTIFICATIONS stay for playback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dot-file filter (AppleDouble `._x.mp3`, `.trashed-*`) and the audio name/type check exist for folder listings. Applied to a file the user picked with the system picker they silently dropped it: found on a phone, where a picked `.trashed-...mp3` never showed up under "Added songs". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The library was built by scanning the whole device through MediaStore: it needed
READ_MEDIA_AUDIOand re-scanned on every cold start and every foreground. It is now built from what the user adds: folders (system folder picker) and single songs (system file picker). Nothing scans by itself; a new file in an added folder shows up after Refresh library. No storage/audio permission any more.What changed
OpenDocumentTree, songs viaOpenMultipleDocuments, both with persisted READ grants.RoomSourceRepositoryis the only place that takes or releases a grant. Adding or removing a source cancels a running scan first (the scanner is single-flight, so a scan in progress would swallow the follow-up request) and rescans afterwards.SafLibraryScannerreplaces the MediaStore scanner: oneDocumentsContractchild query per directory, and the metadata/artwork extractors, normalizer and artwork cache are reused. Reconciliation is source-scoped (SourceReconciler): a stored song is deleted only when its source was listed COMPLETE, so a source that lost access, is unmounted or was listed partially never deletes anything.Song.idstays aLongbut is now a stable 63-bit hash of provider authority + documentId (pinned by a golden-value test because it is a persisted format), so the saved queue, favorites and playlists survive removing and re-adding a folder. Folder listings skip dot-files (._x.mp3,.trashed-*); a file the user picks explicitly is trusted.sourcestable; songs get asourceIdFK withON DELETE CASCADE; the MediaStore columns are gone.MIGRATION_2_3is a deliberate clean start (old songs dropped; playlist items, favorites and history emptied; playlist names kept), with the DDL copied verbatim from the Room-generated code. Source inserts use IGNORE, never REPLACE (REPLACE deletes the parent row and cascades every song).LocalMusicActions. Settings gets a "Music sources" group: a row per folder (song count, remove with confirmation), one aggregate "Added songs" row, Add folder / Add songs, and a hint about the folders Android refuses. A folder that lost access stays listed as "Access lost" and tapping it reopens the picker there. The empty state on Home and on every Library tab offers both actions. "Rescan library" is now "Refresh library"; Home no longer flashes the empty state before Room's first emission; playlist counts only count items whose song is still in the library.perm_*strings, theCheckingPermission/PermissionRequiredscan states).FOREGROUND_SERVICE*andPOST_NOTIFICATIONSstay.Verification
./gradlew testDebugUnitTest: 108 tests, 0 failures. New: stable-id golden values, audio-doc filter, relative paths, unique names, source-scoped reconcile (including "never delete for an unavailable or incomplete source") and the ignore-short prune.user_version3, emptyforeign_key_check,integrity_checkok, and the schema is identical to a fresh install (diffed against the Room-generated DDL).Download(as the Settings hint says)Music/Telegram(1 song) andDownload/YTDLnis(nestedAudio/,Video/,Backups/): 47 songs / 169 MB, exactly what the old MediaStore library held for that folder; the video folder is not imported; the Folders tab showsTelegram/andYTDLnis/Audio/; the scan took 2.2 s for 48 filesforce-stop(persisted grants); cold start runs no scan; sources and songs survive a reinstall3188ce7: a picked.trashed-*file was silently dropped by the folder dot-file filteradded=1; delete it + Refresh:removed=1dumpsys activity permissionsshows the persisted grants released (4 to 0)Notes for the reviewer
allowBackup=true), so access is derived frompersistedUriPermissionsand re-adding the same folder repairs it. The folder picker refuses the storage root andDownloadon Android 11+..nomedia/IS_MUSICare no longer honoured. A moved or renamed file is a new song. The same file reached through two providers (the picker's Audio tab and a folder) can appear twice.SafAudioDataSource,RoomSourceRepository) has no JVM tests because it needs aContentResolver; the device run above covers it.🤖 Generated with Claude Code