Skip to content

feat(media): add AudioMetadataReader.read(ParcelFileDescriptor) overload - #2822

Closed
PonceGL wants to merge 2 commits into
PixelPlayerHQ:masterfrom
PonceGL:chore/p6-audiometadatareader-pfd-overload
Closed

PonceGL wants to merge 2 commits into
PixelPlayerHQ:masterfrom
PonceGL:chore/p6-audiometadatareader-pfd-overload

Conversation

@PonceGL

@PonceGL PonceGL commented Sep 10, 2026

Copy link
Copy Markdown

Stacked on #2821

This branch is cut from chore/p7-fix-audiometautilstest (#2821), not
master directly — it touches the same area (metadata reading) and
shouldn't land without that test class actually running. GitHub can't
target a non-existent branch on the upstream repo, so this PR's diff
includes #2821's commit for now; it'll shrink to just this change once
#2821 merges into master. One of the small independent fixes listed in
#2813.

What

Adds a descriptor-based overload to AudioMetadataReader for reading audio
metadata (title, artist, artwork, lyrics, ...) without needing a filesystem
File — the SAF / downloaded-file path, where a content:// document isn't
guaranteed to have one.

The trap this avoids

The existing read(File) opens its own descriptor and calls
fd.dup().detachFd() for most TagLib calls, but a bare fd.detachFd()
(no dup()) for the last one. That's harmless today only because nothing
touches the descriptor afterwards inside its own use { }. A descriptor
handed in by a caller who keeps using it is a different story:
detachFd() steals the underlying native descriptor, and the caller's own
close() would find it already gone.

Change

  • Extracted the TagLib read + property-map-to-field mapping (~90 lines) out
    of read(File) into a shared private buildAudioMetadata(), parameterized
    by a dupFd: () -> Int lambda — avoids duplicating that mapping logic
    across two functions.
  • Both overloads now go through dup().detachFd() for every TagLib call,
    including the previously-bare last one. read(File)'s old shortcut is
    gone structurally, not just avoided in the new code.
  • read(pfd) never runs the JAudioTagger fallback (it needs a real File,
    which a SAF document may not have) — documented as a known, accepted gap
    on its KDoc. read(File) keeps the full fallback.

Testing

TagLib is a native library; nothing in this repo's JVM tests calls into it
(checked — the one existing TagLib-adjacent test, LyricsRepositoryImplTest,
only covers a pure function around it). This needs a device.

3 new androidTest cases, run on a Pixel_10 AVD, against a 5 KB tagged MP3
(ffmpeg-generated: ID3 title/artist/album + embedded cover):

  • read(pfd) returns the same metadata as read(file) for the same file.
  • the caller's descriptor is still valid after the call (not detached).
  • readArtwork = false skips the embedded cover.

All 3 pass. JVM baseline unaffected (388 tests, 4 pre-existing failures once
#2821 is applied), assembleDebug succeeds.

A bad paste/merge left mimeTypeToFormat_mapsUniversalFormats defined
three times, two of them as local functions nested inside the other
two @test methods. Braces were balanced so it compiled, but a local
function annotated @test compiles to a synthetic exterior$interior
method that JUnit4's runner rejects, taking the whole class down with
InvalidTestClassError.

Un-nests the three functions back to class level. No assertion changed:
verified all 16 by hand against the current mimeTypeToFormat() — they
were already correct, just never executed. mimeTypeToFormat() had zero
coverage; now it has 3 passing tests, exercised for the first time.

check-baseline.sh / BASELINE-TESTS.md updated to drop this from the
known-failures list (local docs, gitignored — not part of this diff).
Adds a descriptor-based overload for reading audio metadata (title,
artist, artwork, lyrics, ...) without needing a filesystem File — the
SAF / downloaded-file path, where a content:// document isn't
guaranteed to have one (C11 of the offline-downloads plan).

Extracted the TagLib read/field-mapping logic (~90 lines) out of
read(File) into a shared private buildAudioMetadata(), parameterized
by a dupFd: () -> Int lambda, instead of duplicating it across both
overloads (GEN-DES-08 — it's the same TagLib property-map -> domain
field mapping, not superficial similarity).

Both overloads now go through pfd.dup().detachFd() for every TagLib
call, including the last one. read(File) used to detachFd() its own
last-owned descriptor directly, which only worked because nothing
touched it afterwards inside its own use {}. The new overload receives
a descriptor it does not own, so that shortcut is no longer safe in
general — folding both paths through the same always-dup() helper
removes the class of bug rather than special-casing it.

No JAudioTagger fallback on the pfd overload: AudioFileIO.read() needs
a real File, which a SAF document may not have. read(File) keeps the
full fallback for the app-private storage backend, which does have
one. Documented as a known, accepted gap on the new overload's KDoc.

Self-review (code-review skill, high effort) surfaced 6 findings; two
applied: both read() overloads now use the existing
PerformanceMetrics.time() helper instead of hand-rolled nanoTime/
finally timing (was duplicated, that helper already exists and is
used elsewhere e.g. MediaItemBuilder.kt), and read(pfd) gained an
optional `label` param so VERBOSE logs of concurrent descriptor reads
stay distinguishable instead of all saying "descriptor". A test
force-unwrap (fromFile.artwork!!) without an assertNotNull guard was
also fixed. Two findings accepted, not changed, and documented: the
refactor adds one extra dup()+close() per artwork read on read(file)'s
pre-existing hot path (correctness-neutral, negligible cost, traded
for removing the fragile bare-detachFd() shortcut structurally); and
dupFd/jAudioTaggerFallbackFile are two independent buildAudioMetadata
params with no compiler-enforced pairing — a sealed MetadataSource
type would close that gap but is more machinery than two call sites
warrant today (GEN-DES-13).

Testing: TagLib is native and nothing in this repo's JVM tests
exercises it (verified — LyricsRepositoryImplTest only covers a pure
function around it), so this needs a device (D-22). 3 new androidTest
cases against a 5 KB tagged MP3 (ffmpeg-generated, embedded ID3 tags +
cover), run on Pixel_10 (AVD): read(pfd) matches read(file), the
caller's descriptor stays valid afterwards, and readArtwork=false
skips the embedded cover. All 3 pass.

JVM baseline unaffected: 388 tests (4 pre-existing failures once P.7 is
applied — this branch is cut from it), assembleDebug succeeds.
@PonceGL
PonceGL force-pushed the chore/p6-audiometadatareader-pfd-overload branch from 5f637ab to 80742a6 Compare September 10, 2026 11:30
@PonceGL

PonceGL commented Sep 10, 2026

Copy link
Copy Markdown
Author

Self-review (code-review, high effort)

Ran a full self-review of this diff before asking for a human one. 6 findings, 3 applied, 2 documented as accepted trade-offs, 1 already covered by this PR's own KDoc:

Applied:

  • Both read() overloads now use the existing PerformanceMetrics.time() helper instead of hand-rolled nanoTime/finally timing — that helper already exists and is used elsewhere (MediaItemBuilder.kt); I'd duplicated the pattern across two call sites instead of reusing it.
  • read(pfd) gained an optional label param — it was hardcoding "descriptor" in the VERBOSE-gated diagnostic logs, so concurrent reads of different descriptors were indistinguishable in logcat.
  • A test assertion force-unwrapped fromFile.artwork!! without an assertNotNull guard first (only fromPfd.artwork had one) — fixed for a clear JUnit failure instead of a raw NPE if it ever regresses.

Accepted, not changed, documented in the commit message:

  • The refactor makes read(file)'s artwork read go through dup() even for what used to be a bare detachFd() on the last call — one extra dup()+close() syscall pair per file, on the library-scan hot path. Correctness-neutral, negligible cost; traded for removing the fragile "last call doesn't need dup()" shortcut structurally rather than special-casing it.
  • buildAudioMetadata's dupFd and jAudioTaggerFallbackFile are two independent parameters with no compiler-enforced pairing — a sealed MetadataSource type would close that gap, but with exactly two call sites today, it's more machinery than the code warrants yet (GEN-DES-13). Noted for later if a third source shows up.

Already covered: the review flagged that read(pfd)'s hardcoded test assertions depend on TagLib alone parsing the fixture correctly, since there's no JAudioTagger fallback on that path — that's exactly the documented, deliberate scope decision on read(pfd)'s own KDoc (no fallback for SAF-style descriptors), not a new gap.

Re-verified after the fixes: assembleDebug succeeds, JVM baseline unaffected (388 tests, 4 pre-existing failures once #2821 is applied), and all 3 androidTest cases pass on the Pixel_10 AVD.

@PonceGL

PonceGL commented Sep 10, 2026

Copy link
Copy Markdown
Author

Closing for now — reorganizing how this work is staged. It'll go through our fork first and we'll propose it upstream again, possibly bundled differently, once the larger feature it's part of is further along. Not a rejection, just a process change on our side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant