Clear the 35 findings the new tools reported

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>
This commit is contained in:
2026-08-22 14:21:15 -05:00
co-authored by Claude Opus 5
parent 496f1c7e73
commit 65a94b4ec1
9 changed files with 251 additions and 7 deletions
+55
View File
@@ -16,6 +16,19 @@ complexity:
CyclomaticComplexMethod:
# Branchy layout code (when/if inside a UI tree) isn't algorithmic complexity.
ignoreAnnotated: ['Composable']
# A flat `when` used as a lookup table scores one point per entry, so MediaProbe's
# demuxer-name -> Container map reads as complexity 21 while having no nesting, no
# state and nothing to follow. That is the metric measuring table size. A `when` with
# real logic in its branches still counts.
ignoreSingleWhenExpression: true
ignoreSimpleWhenEntries: true
# Same reason the model package is excluded from ReturnCount below: there, one branch
# is one documented outcome. ConversionRouter.route scores 17 because the app can give
# 17 distinct answers to "which engine, and why", not because it is hard to follow --
# it is a flat guard chain with a comment per rule. Only this metric and ReturnCount
# are relaxed for that package; LongMethod, NestedBlockDepth, ComplexCondition and the
# rest still apply there.
excludes: ['**/model/**']
TooManyFunctions:
# Screen files group many small @Composable helpers next to their screen, and the codec /
# container matrix files are intentionally operation-rich cohesive APIs. detekt's default of
@@ -24,6 +37,10 @@ complexity:
allowedFunctionsPerFile: 40
allowedFunctionsPerClass: 40
allowedFunctionsPerInterface: 40
# ContainerCapabilities is a single object holding the container x codec matrix and the
# queries over it. Splitting it to satisfy a count would scatter one table across files.
allowedFunctionsPerObject: 40
allowedFunctionsPerEnum: 40
naming:
FunctionNaming:
@@ -36,9 +53,34 @@ style:
ignoreAnnotated: ['Composable']
ignorePropertyDeclaration: true
ignoreNamedArgument: true
# SI thresholds in the byte formatter. Each literal sits on the same line as the unit
# string it belongs to -- `bytes >= 1_000_000 -> "%.1f MB"` -- so a BYTES_PER_MB pair
# (Long and Double, since the compare and the divide need different types) would add
# six names and say nothing the line does not already say. Counts, limits and tuning
# values still flag; these are unit boundaries.
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
- '1e3'
- '1e6'
- '1e9'
- '1_000'
- '1_000_000'
- '1_000_000_000'
ReturnCount:
# Allow guard-clause-style early returns; detekt's default of 2 is overly strict.
max: 4
excludeGuardClauses: true
# The model package IS the decision layer: ConversionRouter picks an engine,
# ContainerCapabilities validates a container x codec pair, the planners pick a
# strategy. Each `return` there is one specific, user-visible reason, and the count
# equals the number of reasons the app can give -- ConversionRouter.route even
# documents that their ORDER is what decides which message the user sees. Collapsing
# them into one exit would bury that. Everywhere else -- UI, workers, engines -- the
# limit still applies.
excludes: ['**/model/**']
LoopWithTooManyJumpStatements:
# Clear early-continue / early-return loops read fine; the default of 1 is strict.
maxJumpCount: 3
@@ -47,3 +89,16 @@ style:
# preconditions.
excludeGuardClauses: true
max: 3
exceptions:
TooGenericExceptionCaught:
# The engine boundaries catch broadly on purpose. MediaProbe drives the platform
# extractor and MediaMetadataRetriever over arbitrary user files; ConversionWorker and
# ConcatWorker wrap Media3 and FFmpeg. All three sit in front of native code that
# reports a malformed file as anything from IllegalArgumentException to
# IllegalStateException to a bare RuntimeException, and the list is not documented.
# Enumerating it would mean guessing, and a guess that is wrong crashes the app on a
# file it could have simply reported as unreadable. Every one of these catches logs
# and handles -- falls back to FFmpeg, or fails the job with a reason -- and the
# SwallowedException rule stays active to keep it that way.
active: false