Skip to content

refactor: convert the Android-specific layer to Kotlin - #388

Draft
tony19 wants to merge 2 commits into
mainfrom
claude/kotlin-android-refactor-f8cwzg
Draft

refactor: convert the Android-specific layer to Kotlin#388
tony19 wants to merge 2 commits into
mainfrom
claude/kotlin-android-refactor-f8cwzg

Conversation

@tony19

@tony19 tony19 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Rebased onto current main (AGP 9.2.1 / Gradle 9.6.1, including the new in-repo static analysis from #436). Main has since absorbed everything this PR originally carried for build/CI/test modernization — often in improved form (Mockito 5 inline mockmaker, test-retry plugin, JDK-matrix CI, dynamic syslog packet count, retired CircleCI) — so the PR is now reduced to its unique contribution in two commits:

1. build: Kotlin support with per-flavor bytecode targets

Wires the standalone Kotlin Android plugin (2.2.20) into the existing Groovy build:

  • Opts out of AGP 9's built-in Kotlin (android.builtInKotlin=false + android.newDsl=false, JetBrains' documented escape hatch): built-in Kotlin validates a single project-wide jvmTarget against the DSL-level compileOptions, which cannot express this project's per-flavor bytecode targets. Revisit when built-in Kotlin gains per-variant JVM targets (the flags are scheduled for removal in AGP 10).
  • explicit-API mode — every public Kotlin declaration states its visibility and types deliberately
  • Kotlin bytecode targets mirror the existing per-flavor javac override: Java 8 for the published jdk8 library classes, Java 11 for the jdk11 variants and all unit tests

2. refactor: Android layer converted to idiomatic Kotlin

Converted under src/main/kotlin (properties, when, use, fun interfaces, null-safety):

  • ch.qos.logback.classic.android: LogcatAppender, SQLiteAppender, BasicLogcatConfigurator, SQLiteLogCleaner, Clock, SystemClock
  • ch.qos.logback.core.android: AndroidContextUtil, SystemPropertiesProxy

The conversion preserves current behavior and API, including all the recent main-branch work: LayoutWrappingEncoder support in LogcatAppender (#376, @DefaultClass retained on setters for Joran), the application-context holder + setApplicationContext() (#383), EXT_FILES_DIR/EXT_CACHE_DIR properties (#181), broken-external-storage tolerance (#315), and platform-driven creation of the app-specific external dirs (#435, ported as AndroidContextUtil.createAppExternalStorageDirs() with subclassability preserved for FileUtilTest).

Java/XML-config interop preserved: property accessors compile to the original getter/setter signatures (Joran reflection unaffected); @JvmStatic/@JvmOverloads keep the static entry points; @JvmName keeps the package-private setClock test hook unmangled; SQLiteLogCleaner is a fun interface so Java anonymous classes still work. All existing Java tests compile and pass unmodified. One deliberate change: AndroidContextUtil.getContext() is now public (Kotlin has no package-private; it pairs naturally with the public setApplicationContext()).

Minor modernization: getPackageInfo uses PackageInfoFlags on API 33+, and SQLiteAppender.stop()/finalize() no longer NPE when the appender never started.

The upstream-derived logback core/classic port (~400 files) intentionally stays in Java so future diffs against upstream logback remain reviewable. This also means the new Checkstyle/PMD static analysis (#436), which scans src/main/java, keeps covering the entire upstream-derived tree; the CodeQL workflow's Java extractor picks up the Kotlin sources as well.

Verification

This sandbox can't reach dl.google.com/maven.google.com, so AGP couldn't run here; verified with the same compilers CI uses, against Maven-Central artifacts:

  • Full main-source compile: kotlinc 2.x -Xexplicit-api=strict at both -jvm-target 1.8 (jdk8 flavor) and 11, plus javac --release 11 for the 422 Java files
  • All 385 test files compile unmodified against the converted classes (with main's current dependency set: Robolectric 4.16.1, Mockito 5.23, greenmail 2.1.9)
  • Ran the Android-layer suites (LogcatAppenderTest, SQLiteAppenderTest, AndroidContextUtilTest — including the new setApplicationContext tests) plus Joran/OptionHelper/ContextInitializer suites, and FileUtilTest against the ported createAppExternalStorageDirs(): all pass except the one case that requires AGP's generated test manifest, which passes in real CI

The CI gate to watch: the built-in-Kotlin opt-out under AGP 9.2.1 hasn't been exercised in CI yet — this run confirms the standalone plugin loads and the per-flavor bytecode check (major 52 for jdk8, 55 for jdk11) still passes.

Relation to #211 (closed WIP "logback-android next")

Reviewed for pull-in candidates: its discrete bug fixes are already on main, and its partial Kotlin conversions are a subset of this PR's. Its unmerged WIP config DSL is better suited to a dedicated follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UCK17vyzha2TXKfm6UJCom

tony19 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Status: GitHub Actions is fully green (assemble + all 993 release-variant unit tests + lint) as of aa12207, after fixing Mockito-on-JDK-17 mocking, the android-all test classpath, two JDK-20+ test incompatibilities, and de-flaking the async socket-appender suite.

CircleCI test and test_app are still red, and I can't see their logs from this environment (only the CircleCI UI has them, via store_test_results). Two asks when you get a chance, @tony19:

  1. Could you paste the failing test names from the CircleCI test job? The identical suite passes on GitHub Actions on the same commit, so I suspect either a debug-variant-only issue or executor flakiness — with the list I can fix it directly.
  2. test_app builds tony19-sandbox/logback-test-app against the new AAR. This PR moves the library to Java 17 bytecode (AGP 8 baseline), so that app likely needs AGP 7.4+/JDK 17 to consume it — its toolchain is outside this repo, so it needs a change there (happy to advise on the specifics).

🤖 Generated with Claude Code


Generated by Claude Code

@tony19
tony19 force-pushed the claude/kotlin-android-refactor-f8cwzg branch from fad499b to 7114177 Compare July 11, 2026 23:59
@tony19 tony19 changed the title refactor: modern Kotlin for the Android layer + AGP 8 / SDK 35 build modernization refactor: convert the Android-specific layer to Kotlin Jul 11, 2026
@tony19
tony19 force-pushed the claude/kotlin-android-refactor-f8cwzg branch 6 times, most recently from d9a36ea to aabe4fb Compare July 13, 2026 04:51
claude added 2 commits July 13, 2026 21:41
…ilt-in support

This project publishes JDK-specific artifacts whose flavors pin different
bytecode targets (jdk8 -> Java 8, jdk11 -> Java 11). AGP 9's built-in
Kotlin support pins a single jvmTarget per project, validated against the
DSL-level compileOptions (issuetracker.google.com/408242956), so it cannot
express that layout: aligning Kotlin with either flavor breaks the other,
and raising javac per-task via options.release detaches the task from the
Android bootclasspath.

Instead, opt out of built-in Kotlin (android.builtInKotlin=false +
android.newDsl=false — JetBrains' documented escape hatch, scheduled for
removal in AGP 10; revisit when built-in Kotlin gains per-variant JVM
targets) and use the standalone Kotlin Android plugin (2.2.20):

- Kotlin bytecode targets mirror the existing per-flavor javac override
  exactly: Java 8 for the published jdk8 library classes, Java 11 for the
  jdk11 variants and all unit tests. The javac override itself is
  untouched.
- The standalone plugin's task-pairing JVM-target validation is demoted
  to a warning (kotlin.jvm.target.validation.mode): it reads the paired
  Java task's DSL-derived targetCompatibility, which intentionally
  diverges for jdk8. The published class-file versions are enforced
  independently by the CI workflow's bytecode check.
- Library Kotlin sources compile in explicit-API mode, so every public
  declaration states its visibility and types deliberately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCK17vyzha2TXKfm6UJCom
Convert the Android integration layer (the code unique to
logback-android, as opposed to the upstream logback port) to modern,
idiomatic Kotlin under src/main/kotlin:

- LogcatAppender, SQLiteAppender, BasicLogcatConfigurator,
  SQLiteLogCleaner, Clock, SystemClock (ch.qos.logback.classic.android)
- AndroidContextUtil, SystemPropertiesProxy (ch.qos.logback.core.android)

The conversion preserves current behavior and API, including the recent
LayoutWrappingEncoder support in LogcatAppender (issue #376, with
@defaultclass retained on the setters for Joran), the application-context
holder and setApplicationContext() (issue #383), the EXT_FILES_DIR /
EXT_CACHE_DIR properties (issue #181), and the broken-external-storage
tolerance (issue #315), and the
platform-mediated creation of app-specific external dirs
(createAppExternalStorageDirs, issue #228) that FileUtil calls into.

Java interop is preserved: property accessors compile to the original
getter/setter signatures (Joran reflection unaffected),
@JvmStatic/@jvmoverloads retain the static entry points, @JvmName keeps
the package-private setClock test hook unmangled, and SQLiteLogCleaner
is a fun interface so Java anonymous classes still work. All existing
Java tests compile and pass unmodified against the converted classes.
One deliberate change: AndroidContextUtil.getContext() is now public
(Kotlin has no package-private; it pairs naturally with the public
setApplicationContext()).

Minor modernization: getPackageInfo uses PackageInfoFlags on API 33+,
and SQLiteAppender.stop()/finalize() no longer NPE when the appender
never started. The upstream-derived logback core/classic port
intentionally stays in Java to keep diffs against upstream reviewable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCK17vyzha2TXKfm6UJCom
@tony19
tony19 force-pushed the claude/kotlin-android-refactor-f8cwzg branch from aabe4fb to f19fd4f Compare July 13, 2026 21:42
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.

2 participants