detekt found 29 and Android lint 6, on a codebase neither had ever seen. Each
one was either fixed or relaxed with the reason written next to it; nothing was
suppressed to make the build quiet.
Fixed, because the tool was right:
- ConversionDependencies constructs Media3Engine, which is @UnstableApi, and
was not marked. Every other type here that touches Media3 propagates the
marker rather than swallowing it with @OptIn, so this one does too. Lint
was the only thing that had ever noticed.
- MediaProbe converted microseconds to milliseconds with a bare 1000, twice,
in a file that also handles a seconds-based duration from a different API.
US_PER_MS and MS_PER_SECOND now say which is which -- that confusion is a
real bug source in media code, not a style question.
- Foreground service types compared SDK_INT against 34 and 35 as raw ints
while the doc comment above spelled the version names out. VERSION_CODES
says it in the code.
- take(3) is a product decision about how many alternatives an error offers.
It means nothing until it is named; MAX_SUGGESTIONS does.
- setProgress(100, ...) is a percentage max, now PERCENT_MAX.
Relaxed, because the rule did not fit:
- The model package is excluded from ReturnCount and CyclomaticComplexMethod
ONLY. It is the decision layer: ConversionRouter.route scores 17 because
the app can give 17 distinct answers to "which engine, and why", each with
its own user-visible reason, and route's own comment records that their
ORDER decides which message is shown. Counting those as complexity measures
how many answers exist, not how hard the code is to follow. Everything else
-- LongMethod, NestedBlockDepth, ComplexCondition -- still applies there.
- A flat `when` used as a lookup table scores a point per entry, so
MediaProbe's demuxer-name to Container map read as complexity 21 with no
nesting and no state. ignoreSingleWhenExpression is the rule's own answer.
- TooGenericExceptionCaught off. MediaProbe, ConversionWorker and ConcatWorker
sit in front of native code that reports a malformed file as anything from
IllegalArgumentException to a bare RuntimeException, undocumented.
Enumerating that list means guessing, and a wrong guess crashes the app on a
file it could have reported as unreadable. SwallowedException stays on, so
these still have to log and handle.
- SI thresholds in the byte formatter, via ignoreNumbers. Each literal sits on
the line with the unit string it belongs to; BYTES_PER_MB would need a Long
and a Double and say nothing the line does not.
- allowedFunctionsPerObject, which the first pass simply missed.
Lint's three version-freshness nags are off. They do not describe this code --
they go red the day someone else publishes a release, which turns a PR red for
something its author cannot see in their diff, and they want the network at
lint time. Upgrades here are deliberate; Kotlin in particular is pinned to AGP's
bundled KGP and is not free to follow the newest release.
UsableSpace is informational rather than disabled, because it is a real finding
that this commit is choosing not to act on. hasSpaceFor reads File.usableSpace,
which ignores reclaimable cache, so the app can refuse a conversion it had room
for. StorageManager.getAllocatableBytes is the better answer, but it changes
when a job is rejected and can throw -- a behaviour change to a safety check,
which deserves its own commit and its own test rather than a drive-by here.
informational keeps it in every lint report instead of hiding it.
Also adds the CI gate and a CLAUDE.md. Coverage is reported and not gated: the
measured baseline is 31% of lines, which is exactly why LibreMail's 0.84 floor
was evidence about LibreMail and not a number to copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
LibreMediaConverter is an Android media converter (Kotlin, Jetpack Compose, Material 3) with two
conversion engines: Media3 Transformer for the hardware path and FFmpeg for everything the platform
cannot do. See @README.md for the architecture and @LICENSES/README.md for the split license —
this file covers only what is not obvious from the code.
Build, test, lint
Use a JDK 17–21 for the Gradle daemon. AGP 9 will not run on anything older than 17, and does
not support 25+ — if JAVA_HOME points at 25, builds fail.
./gradlew :app:assembleDebug # build debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests
./gradlew :app:ktlintCheck # formatting
./gradlew :app:ktlintFormat # fix formatting in place
./gradlew :app:detekt # static analysis
./gradlew :app:lintDebug # Android lint
./gradlew :app:jacocoTestReport # coverage (XML+HTML under app/build/reports/jacoco/)
# single unit test:
./gradlew :app:testDebugUnitTest --tests "org.libremediaconverter.model.ConversionRouterTest"
CI's "Static analysis" gate is exactly ./gradlew :app:ktlintCheck :app:detekt :app:lintDebug --continue. Run it with --continue locally too: one round trip gives you all three lists instead
of the first one that fails.
Before treating a change as done, run: assembleDebug + testDebugUnitTest +
compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug.
compileDebugAndroidTestKotlin matters more here than it looks — the instrumented suite cannot run
on this machine (below), so without it an androidTest compile error is not discovered until CI.
ktlint and detekt also cover the test/androidTest source sets that lintDebug skips.
Instrumented tests do not run locally
Two independent reasons, so do not spend time on either:
- Emulators segfault on this host. qemu dies on every AVD. Instrumented tests run on CI or on the physical Pixel, never in a local emulator.
- The API 37 image is broken.
android-37.0crash-loops surfaceflinger inside its own gralloc mapper, so every test fails there regardless of this app.docs/api-37-emulator-crash.mdrecords the evidence and the ruled-out fixes; CI's matrix therefore stops at API 36 even though targetSdk is 37. API 37 needs a manual check on the Pixel 10 Pro XL before each release.
On a device or emulator, build only the ABI it can execute:
./gradlew :app:connectedDebugAndroidTest -PabiFilters=x86_64
FFmpeg's native libraries dominate the APK, so shipping arm64 to an x86_64 emulator doubles the install for code that can never run — and on API 37 the full APK does not fit at all.
Conventions
- ktlint owns formatting, detekt owns static analysis. detekt's formatting ruleset is off, so
the two can never disagree about the same line. Never hand-fix a formatting complaint — run
ktlintFormat. Style isintellij_ideaat 120 columns, set in.editorconfig. - detekt config is
config/detekt/detekt.yml, merged onto detekt's defaults (buildUponDefaultConfig = true), so it carries only the rules this codebase legitimately breaks — each with the reason written next to it. Relax a rule that way or fix the code; never a bare@Suppress. Do not invent config keys: unknown ones are rejected. - The
modelpackage is excluded fromReturnCountandCyclomaticComplexMethodonly. It is the decision layer, where one branch is one documented user-visible outcome and the metric counts answers rather than complexity. Every other rule still applies there. - Coverage is reported, not gated — currently ~31% of lines. A floor needs a baseline that has settled first.
kotlin.code.style=official. Gradle stays Kotlin DSL.
Traps
- Do not apply
org.jetbrains.kotlin.android. AGP 9 has built-in Kotlin; applying the legacy plugin fails the build. This is whylibs.versions.tomlpinskotlinto AGP's bundled KGP version rather than the newest Kotlin release — the Compose compiler plugin must match it. - The FFmpeg AAR is committed under
bin/, deliberately. It is not on any Maven repo (ffmpeg-kit was archived and delisted). Rebuilding per CI run made red builds ambiguous: broken code, or a cross-compile that hiccuped?bin/README.mdhas provenance and how to regenerate it. - Anything touching Media3 carries
@UnstableApirather than swallowing the marker with@OptIn. Android lint'sUnsafeOptInUsageErrorcatches a missed one. - Release builds ship both ABIs.
-PabiFiltersis a test-run override only;build.ymlverifies the released APK carries every ABI and that all native libraries are 16 KB aligned.