From 5b26ed9ef2b01fa9cbe95b8e8251127ef7e965dd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 12:54:54 +0200 Subject: [PATCH 01/11] wip Signed-off-by: alperozturk96 --- .../skills/android-java-to-kotlin/SKILL.md | 159 ++++++++ .../assets/android-checklist.md | 63 +++ .../assets/checklist.md | 60 +++ .../assets/worked-example.md | 163 ++++++++ .../references/ANDROID-IDIOMS.md | 161 ++++++++ .../references/CONCURRENCY.md | 120 ++++++ .../references/CONVERSION-METHODOLOGY.md | 352 +++++++++++++++++ .../references/FAIL-FAST.md | 108 ++++++ .../references/KNOWN-ISSUES.md | 358 ++++++++++++++++++ .../references/PROJECT-CONVENTIONS.md | 74 ++++ .../references/TESTING.md | 107 ++++++ .../references/frameworks/DAGGER-HILT.md | 160 ++++++++ .../references/frameworks/JUNIT.md | 193 ++++++++++ .../references/frameworks/MOCKITO.md | 253 +++++++++++++ AGENTS.md | 7 + 15 files changed, 2338 insertions(+) create mode 100644 .claude/skills/android-java-to-kotlin/SKILL.md create mode 100644 .claude/skills/android-java-to-kotlin/assets/android-checklist.md create mode 100644 .claude/skills/android-java-to-kotlin/assets/checklist.md create mode 100644 .claude/skills/android-java-to-kotlin/assets/worked-example.md create mode 100644 .claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md create mode 100644 .claude/skills/android-java-to-kotlin/references/CONCURRENCY.md create mode 100644 .claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md create mode 100644 .claude/skills/android-java-to-kotlin/references/FAIL-FAST.md create mode 100644 .claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md create mode 100644 .claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md create mode 100644 .claude/skills/android-java-to-kotlin/references/TESTING.md create mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md create mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md create mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md new file mode 100644 index 000000000000..bfe109cdbd4a --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -0,0 +1,159 @@ +--- +name: android-java-to-kotlin +description: > + Use when finishing a Java-to-Kotlin conversion in an Android project, when the user + mentions "java to kotlin", "j2k", "convert java", "migrate java to kotlin", "finish the + conversion", "make it idiomatic", or when a freshly IDE-converted .kt file needs to be + turned into clean, modern, idiomatic Kotlin. The developer first runs the IDE converter + (Android Studio: Code > Convert Java File to Kotlin File), then this skill drives the + second pass: idiomatic cleanup, fail-fast control flow, coroutines/lifecycleScope, + modern Android APIs, function decomposition, and a behaviour-locking test. +license: AGPL-3.0-or-later +metadata: + author: Nextcloud Android + version: "1.0.0" + based-on: JetBrains kotlin-tooling-java-to-kotlin (Apache-2.0) +--- + +# Android Java to Kotlin Conversion (Second Pass) + +The IDE converter produces Kotlin that *compiles* but is not *idiomatic*: platform types +everywhere, one giant function per lifecycle callback, nested `if`/`else`, `new Thread`, +`switch`, `TextUtils.isEmpty`, magic numbers. This skill drives the disciplined second +pass that turns that output into clean, modern, testable Kotlin — **without changing +observable behaviour** — and then writes a test that proves behaviour is unchanged. + +## The Two-Person Workflow + +```dot +digraph android_j2k { + rankdir=TB; + "Developer: IDE converts .java -> .kt" -> "Step 0: Establish baseline"; + "Step 0: Establish baseline" -> "Step 1: Detect frameworks"; + "Step 1: Detect frameworks" -> "Step 2: Idiomatic pass"; + "Step 2: Idiomatic pass" -> "Step 3: Write behaviour-locking test"; + "Step 3: Write behaviour-locking test" -> "Step 4: Verify (build + checks + tests)"; + "Step 4: Verify (build + checks + tests)" -> "Done" [label="green"]; + "Step 4: Verify (build + checks + tests)" -> "Step 2: Idiomatic pass" [label="fail / behaviour drift"]; +} +``` + +The **developer** runs the mechanical IDE conversion (`Code > Convert Java File to Kotlin +File`, or ⌥⇧⌘K). **You (Claude)** complete everything after that. If you are handed a +`.java` file instead, first apply the faithful 1:1 translation in +[CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md) to reach the same +starting point, then continue. + +## The Prime Directive: Behaviour Must Not Change + +Every transformation in this skill is **behaviour-preserving**. You are refactoring for +readability, safety, and modern API usage — not adding features or fixing bugs. If you +spot a real bug, surface it to the developer; do not silently "fix" it inside a +conversion. The behaviour-locking test in Step 3 exists to keep you honest. + +The 5 invariants from [CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md) +still apply: no new side-effects, preserve annotations/targets, preserve package, +preserve documentation (as KDoc), output valid Kotlin. + +## Step 0: Establish Baseline + +Before editing anything: + +1. Read the whole `.kt` file (and, if you can, the original `.java` via + `git show :.java` or the IDE's local history) to understand *what it does*. +2. Write down the **public/observable surface** you must keep intact: + - Public and `@VisibleForTesting` method signatures called from other classes (Java + callers especially — see `@JvmStatic`/`@JvmField` in + [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md)). + - Lifecycle callbacks (`onCreate`, `onViewCreated`, `onSaveInstanceState`, …) and their + ordering of side-effects. + - Any Parcelable/Bundle keys, intent extras, and `newInstance(...)` factory shapes. +3. Note threading: which work runs off the main thread today (`Thread`, `AsyncTask`, + `runOnUiThread`, executors) — this maps to coroutines in Step 2. + +## Step 1: Detect Frameworks + +Scan imports and load ONLY the matching guides. + +| Import prefix | Guide | +|---|---| +| `dagger.*`, `javax.inject.*` | [DAGGER-HILT.md](references/frameworks/DAGGER-HILT.md) | +| `retrofit2.*`, `okhttp3.*` | [RETROFIT.md](references/frameworks/RETROFIT.md) | +| `io.reactivex.*`, `rx.*` | [RXJAVA.md](references/frameworks/RXJAVA.md) | +| `org.junit.*` (test files) | [JUNIT.md](references/frameworks/JUNIT.md) | +| `org.mockito.*` (test files) | [MOCKITO.md](references/frameworks/MOCKITO.md) | +| `com.fasterxml.jackson.*` | [JACKSON.md](references/frameworks/JACKSON.md) | +| `org.springframework.*` / `lombok.*` / `*.persistence.*` / `io.micronaut.*` / `io.quarkus.*` / `com.google.inject.*` | see `references/frameworks/` (rare in Android app code) | + +## Step 2: Idiomatic Pass + +Apply, in this order, then re-check the invariants: + +1. **Kill platform types.** Give every `!` platform type an explicit nullable/non-null + type based on the Java source and call sites. See "Platform Types" in + [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md). +2. **Fail fast.** Replace nested `if`/`else` pyramids and null-checks with guard clauses + and `require`/`requireNotNull`/`?: return`. See [FAIL-FAST.md](references/FAIL-FAST.md). +3. **Decompose.** Break each oversized lifecycle callback / `setupView`-style method into + small, single-purpose private functions named for their intent. See + [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md). +4. **Modern concurrency.** Replace `new Thread`, `AsyncTask`, `runOnUiThread`, and + callback pairs with `lifecycleScope` + `suspend` + `withContext`. See + [CONCURRENCY.md](references/CONCURRENCY.md). +5. **Modern Android + Kotlin idioms.** Scope functions (`run`/`apply`/`let`), extension + functions, `when`/`partition`/`filter` over `switch`, string templates, `isNullOrEmpty`, + view/KTX extensions. See [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md). +6. **Project conventions.** SPDX header, no magic numbers (`companion object` + + `const val`), resources not hardcoded strings, ≤300 lines/file, ≤120 cols. See + [PROJECT-CONVENTIONS.md](references/PROJECT-CONVENTIONS.md). +7. **Testability seams.** Extract pure logic (URL building, permission math, partitioning) + into functions you can unit-test; mark with `@VisibleForTesting` where they must stay + `internal`/`private`-ish but be reachable from tests. + +Do NOT expand scope. Unrelated files stay untouched (AGENTS.md / AI policy). + +## Step 3: Write a Behaviour-Locking Test (Mandatory) + +A conversion is not complete until a test proves behaviour is unchanged. See +[TESTING.md](references/TESTING.md) for the decision tree. In short: + +- **Pure functions you extracted** (e.g. link builders, `isReshareForbidden`, + partitioning) → fast JVM unit tests in `app/src/test/` (JUnit4 + mockito-kotlin). +- **Fragment/Activity/DB behaviour** → instrumented test in `app/src/androidTest/` + extending the project's base test class, or a Robolectric test where the project uses it. +- Prefer testing the **seams you just created**. If the Java original had no test, your + new test is the characterization test that locks current behaviour. + +Every test file gets the SPDX header and follows the project's test conventions. + +## Step 4: Verify + +Run and report real output — never claim green without evidence: + +```bash +# Formatting + static analysis on the changed files +./gradlew spotlessKotlinCheck detektGplayDebug lintGplayDebug spotbugsGplayDebug + +# Unit tests +./gradlew jacocoTestGplayDebugUnitTest + +# Instrumented tests (if you wrote one), scoped to the class +./gradlew createGplayDebugCoverageReport -Pcoverage=true \ + -Pandroid.testInstrumentationRunnerArguments.class= +``` + +Then walk [assets/android-checklist.md](assets/android-checklist.md). If anything fails or +behaviour drifted, return to Step 2. + +## Worked Example + +[assets/worked-example.md](assets/worked-example.md) is a real before/after from +`FileDetailSharingFragment` (871-line Java fragment → idiomatic Kotlin) showing each +transformation class in context. Read it when you need a concrete pattern. + +## Batch Conversion + +Convert one file at a time, leaf dependencies first. Report progress per file. Warn the +developer before a change set grows into several thousand lines — split into focused PRs +(AGENTS.md). Each commit needs `Assisted-by: :`; only the human adds +`Signed-off-by`. diff --git a/.claude/skills/android-java-to-kotlin/assets/android-checklist.md b/.claude/skills/android-java-to-kotlin/assets/android-checklist.md new file mode 100644 index 000000000000..848ba170526f --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/assets/android-checklist.md @@ -0,0 +1,63 @@ +# Android Post-Conversion Checklist + +Walk this after Step 2–3, before declaring done. Complements the generic +[checklist.md](checklist.md) (compilation, annotations, imports, nullability, collections). + +## Behaviour Preserved (Prime Directive) +- [ ] Same public / `@VisibleForTesting` signatures as before (Java callers still compile) +- [ ] Same exception types AND messages for the same preconditions +- [ ] Same Bundle keys, intent extras, and `newInstance(...)` wiring +- [ ] Same branch outcomes and same order of side-effects (snackbars, logs, DB writes) +- [ ] No feature added, no bug silently fixed inside the conversion + +## Fail Fast +- [ ] Nested `if`/`else` pyramids replaced with guard clauses / early returns +- [ ] `require` / `requireNotNull` / `check` used for preconditions (matching Java throw type) +- [ ] Resource cleanup (`cursor.close()`, streams) still runs on every early-return path + (or converted to `use {}`) + +## Function Decomposition +- [ ] Oversized lifecycle callbacks split into small, intent-named private functions +- [ ] Duplicated blocks factored into parameterized helpers +- [ ] File ≤300 lines (or split raised with developer / justified suppression noted) + +## Concurrency +- [ ] `new Thread` / `AsyncTask` / `runOnUiThread` replaced with `lifecycleScope` + `suspend` +- [ ] Correct dispatcher (`IO` for disk/net/DB, `Main` for views) +- [ ] `binding == null` / lifecycle guards preserved inside `launch` +- [ ] No `GlobalScope`; `CancellationException` not swallowed +- [ ] Timing-sensitive callers checked (sync DB read → async is flagged if observable) + +## Modern Android + Kotlin Idioms +- [ ] Platform types (`!`) all given explicit nullability +- [ ] Scope functions (`run`/`apply`/`let`) replace repeated `binding.`/`viewThemeUtils.` chains +- [ ] `switch` → `when` / `filter` + `partition` +- [ ] `TextUtils.isEmpty` → `isNullOrEmpty`; verbose utils → extension functions / KTX +- [ ] Getter/setter method calls → Kotlin property access + +## Project Conventions +- [ ] SPDX header replaced with current template + correct year +- [ ] Magic numbers → `const val` in `companion object` +- [ ] No hardcoded strings/colors/dimens (resources only; strings.xml only) +- [ ] `@JvmStatic` / `@JvmField` / `@JvmOverloads` / `@Throws` where Java calls in +- [ ] ≤120 cols, one type per file, exactly one trailing newline +- [ ] No decorative divider comments + +## Tests (Mandatory) +- [ ] Behaviour-locking test written (unit for pure logic, instrumented for components) +- [ ] Existing tests for this class still pass +- [ ] Test file has SPDX header and follows project test conventions +- [ ] Test asserts the observable contract, not implementation details + +## Verification Run +- [ ] `spotlessKotlinCheck` clean on changed files +- [ ] `detektGplayDebug` clean on changed files +- [ ] `lintGplayDebug` clean on changed files +- [ ] `spotbugsGplayDebug` clean on changed files +- [ ] Unit tests green (`jacocoTestGplayDebugUnitTest`) +- [ ] Actual command output reported (no unverified "it's green") + +## Git History +- [ ] `git mv` rename committed separately from content change +- [ ] Conventional Commit message + `Assisted-by:` trailer +- [ ] No `Signed-off-by` added by the agent; no autonomous PR/issue diff --git a/.claude/skills/android-java-to-kotlin/assets/checklist.md b/.claude/skills/android-java-to-kotlin/assets/checklist.md new file mode 100644 index 000000000000..6b629b3262b6 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/assets/checklist.md @@ -0,0 +1,60 @@ +# Post-Conversion Verification Checklist + +Use this checklist after converting each Java file to Kotlin. + +## Compilation & Tests +- [ ] The `.kt` file compiles without errors +- [ ] All existing tests still pass +- [ ] No new compiler warnings introduced + +## Semantic Correctness +- [ ] No new side-effects or behavioural changes +- [ ] All public API signatures preserved (method names, parameter types, return types) +- [ ] Exception behaviour unchanged (same exceptions thrown in same conditions) + +## Annotations +- [ ] All annotations preserved from the original Java code +- [ ] Annotation site targets correct (`@field:`, `@get:`, `@set:`, `@param:`) +- [ ] No annotations accidentally dropped during conversion + +## Imports & Package +- [ ] Package declaration matches original +- [ ] All imports carried forward (except Java types that shadow Kotlin builtins) +- [ ] No new imports added unnecessarily + +## Documentation +- [ ] All Javadoc converted to KDoc format +- [ ] `{@code ...}` → backtick code in KDoc +- [ ] `{@link ...}` → `[...]` KDoc links +- [ ] `

` paragraph tags → blank lines +- [ ] `@param`, `@return`, `@throws` tags preserved +- [ ] Class-level and method-level documentation preserved + +## Nullability & Mutability +- [ ] Non-null types used only where provably non-null +- [ ] Nullable types (`?`) used for all Java types that could be null +- [ ] `val` used for all immutable variables/properties +- [ ] `var` used only for mutable variables/properties + +## Collections +- [ ] `MutableList`/`MutableSet`/`MutableMap` for Java's mutable collections +- [ ] `List`/`Set`/`Map` only where Java used immutable wrappers + +## Kotlin Idioms +- [ ] Getters/setters replaced with Kotlin properties where appropriate +- [ ] String concatenation replaced with string templates where clearer +- [ ] Elvis operator used where appropriate +- [ ] `when` expression used instead of `switch` +- [ ] Smart casts used after `is` checks (no explicit casts) + +## Framework-Specific (check applicable items) +- [ ] **Spring**: Classes that need proxying are `open`; `@Bean` methods are `open` +- [ ] **Lombok**: All Lombok annotations removed; replaced with Kotlin equivalents +- [ ] **Hibernate/JPA**: Entities are `open` (not data classes); no-arg constructor provided +- [ ] **Jackson**: `@field:` and `@get:` annotation site targets correct +- [ ] **RxJava**: Reactive types correctly mapped to Coroutines/Flow +- [ ] **Mockito**: `when` keyword escaped or replaced with MockK equivalent + +## Git History +- [ ] File renamed via `git mv` (not delete + create) +- [ ] Rename commit separate from content change commit diff --git a/.claude/skills/android-java-to-kotlin/assets/worked-example.md b/.claude/skills/android-java-to-kotlin/assets/worked-example.md new file mode 100644 index 000000000000..58d1ce0379d9 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/assets/worked-example.md @@ -0,0 +1,163 @@ +# Worked Example: FileDetailSharingFragment + +A real conversion of an 871-line Java fragment +(`com.owncloud.android.ui.fragment.FileDetailSharingFragment`) into idiomatic Kotlin. Each +section shows one transformation class from the skill, in context. Behaviour is unchanged +throughout; the only new capability (`@VisibleForTesting createInternalLink`) is a +testability seam that returns the same URL the Java produced. + +## A. SPDX Header Rewrite + +```diff +-/* +- * Nextcloud Android client application +- * +- * @author Andy Scherzinger +- * ... +- * Copyright (C) 2018 Andy Scherzinger +- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only +- */ ++/* ++ * Nextcloud - Android Client ++ * ++ * SPDX-FileCopyrightText: 2026 Alper Ozturk ++ * SPDX-License-Identifier: AGPL-3.0-or-later ++ */ +``` + +## B. Fail-Fast Preconditions + +```diff +-if (file == null) throw new IllegalArgumentException("File may not be null"); +-if (user == null) throw new IllegalArgumentException("Account may not be null"); +-fileActivity = (FileActivity) getActivity(); +-if (fileActivity == null) throw new IllegalArgumentException("FileActivity may not be null"); ++fileActivity = (activity as FileActivity?) ++requireNotNull(file) { "File may not be null" } ++requireNotNull(user) { "Account may not be null" } ++requireNotNull(fileActivity) { "FileActivity may not be null" } +``` + +## C. Decomposition of `onViewCreated` + +The Java `onViewCreated` inlined adapter creation (duplicated for internal/external), +layout managers, listeners, and the fetch kick-off. It became a readable sequence plus +extracted helpers `getUserId()`, `setupInternalShares()`, `setupExternalShares()`, +`createShareListAdapter(userId, type)`, `startAnimation()`. The two near-identical adapter +blocks collapsed into one parameterized factory: + +```kotlin +private fun createShareListAdapter(userId: String, type: SharesType): ShareeListAdapter = + ShareeListAdapter( + fileActivity!!, ArrayList(), this, userId, user, viewThemeUtils, + (file?.isEncrypted == true), type + ).apply { setHasStableIds(true) } +``` + +## D. `new Thread` + `runOnUiThread` → `lifecycleScope` + `suspend` + +```diff +-private void fetchE2EECounter(Runnable onComplete) { +- new Thread(() -> { +- try { ... fileDataStorageManager.saveFile(file); } +- catch (Exception e) { Log_OC.e(TAG, "..." + e.getMessage()); } +- Activity a = getActivity(); +- if (a != null) a.runOnUiThread(onComplete); +- }).start(); +-} ++private suspend fun fetchE2EECounter(): Boolean = withContext(Dispatchers.IO) { ++ return@withContext try { ++ val client = clientFactory.create(user) ++ val metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, requireContext()) ++ if (metadata is DecryptedFolderMetadataFile) { ++ file?.setE2eCounter(metadata.metadata.counter) ++ fileDataStorageManager?.saveFile(file) ++ } ++ true ++ } catch (e: Exception) { ++ Log_OC.e(TAG, "Error refreshing E2E counter: " + e.message) ++ false ++ } ++} +``` + +Caller now `lifecycleScope.launch { if (!fetchE2EECounter()) return@launch; withContext(Main){ ... } }`. +The callback-pair `fetchSharees(onSuccess, onError)` was likewise turned into a `suspend` +call returning `Boolean`. + +## E. `switch` Bucketing → `partition` + Set + +```diff +-for (OCShare share : shares) { +- if (share.getShareType() != null) { +- switch (share.getShareType()) { +- case PUBLIC_LINK: case FEDERATED_GROUP: case FEDERATED: case EMAIL: +- externalShares.add(share); break; +- default: internalShares.add(share); break; +- } +- } +-} ++private val externalShareTypes = setOf( ++ ShareType.PUBLIC_LINK, ShareType.FEDERATED_GROUP, ShareType.FEDERATED, ShareType.EMAIL ++) ++val (external, internal) = shares ++ .filter { it.shareType != null } ++ .partition { it.shareType in externalShareTypes } +``` + +## F. Nested `if` Cursor Handling → Guard Clauses + +The three-level-nested `handleContactResult` became flat sequential guards, each showing +the snackbar + log and returning, with `cursor.close()` preserved on every path. See +[FAIL-FAST.md](../references/FAIL-FAST.md) section "Deeply Nested". + +## G. Scope Functions & KTX + +```diff +-final LinearLayout shimmerLayout = binding.shimmerLayout.getRoot(); +-shimmerLayout.clearAnimation(); +-shimmerLayout.setVisibility(View.GONE); +-binding.shareContainer.setVisibility(View.VISIBLE); ++binding?.run { ++ shimmerLayout.root.run { clearAnimation(); visibility = View.GONE } ++ shareContainer.visibility = View.VISIBLE ++} +``` + +```diff +-for (int i = 0; i < viewGroup.getChildCount(); i++) { toggleSearchViewEnable(viewGroup.getChildAt(i), enable); } ++for (i in 0.. 3); ++companion object { ++ private const val TAG = "FileDetailSharingFragment" ++ private const val MIN_SHOW_ALL_VISIBLE_ITEM_COUNT = 3 ++ @JvmStatic fun newInstance(file: OCFile?, user: User?) = FileDetailSharingFragment().apply { ... } ++} ++binding.sharesListInternalShowAll.setVisibleIf(internalShares.size > MIN_SHOW_ALL_VISIBLE_ITEM_COUNT) +``` + +## I. Testability Seam + +`createInternalLink` was inlined string concatenation in Java. It was extracted to a pure +`@VisibleForTesting internal fun createInternalLink(user, file, capabilities): String` that +returns the identical URL — enabling the unit test in +[TESTING.md](../references/TESTING.md) without a device. + +## J. Detekt Suppression (Last Resort) + +The class is genuinely large and can't be fully split within one conversion, so a +file-level suppression documents the debt honestly: + +```kotlin +@Suppress("TooManyFunctions", "LargeClass", "TooGenericExceptionCaught", "ReturnCount") +class FileDetailSharingFragment : Fragment(), ... { +``` + +Prefer real decomposition; use this only when splitting is out of scope, and tell the +developer. diff --git a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md new file mode 100644 index 000000000000..40d87b462f7e --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md @@ -0,0 +1,161 @@ +# Android + Kotlin Idioms + +Transformations applied during the idiomatic pass. Every one is behaviour-preserving. +Examples are drawn from a real fragment conversion. + +## 1. Decompose Oversized Functions + +The IDE keeps the Java structure: one enormous `onViewCreated`/`setupView` that inflates, +themes, wires listeners, and kicks off loading in a single 80-line block. Split by +intent into small private functions. The lifecycle callback becomes a readable table of +contents. + +```kotlin +// BEFORE: onViewCreated does everything inline (adapters, layout managers, listeners, fetch) + +// AFTER +override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + fileActivity ?: return + fileDataStorageManager = fileActivity?.storageManager + fileOperationsHelper = fileActivity?.fileOperationsHelper + + startAnimation() + val userId = getUserId() + setupInternalShares(userId) + setupExternalShares(userId) + binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } + fetchSharees() + setupView() +} +``` + +Rules: +- One function = one reason to change. Name it for *what it accomplishes* + (`setupInternalShares`, `themeView`, `disableE2EEShareForV1`), not *how*. +- Factor duplicated blocks into a parameterized helper + (`createShareListAdapter(userId, SharesType.INTERNAL)`). +- Keep files ≤300 lines (project rule). Heavy decomposition sometimes means splitting a + god-class into collaborators — raise that with the developer rather than exceeding 300. + +## 2. Scope Functions Over Repetition + +Replace repeated `binding.x` / `viewThemeUtils.material.y` chains with `run`/`apply`/`with`. + +```kotlin +// BEFORE +viewThemeUtils.material.themeSearchCardView(binding.searchCardWrapper); +viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.sendCopyBtn); +viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListInternalShowAll); + +// AFTER +binding.run { + viewThemeUtils.material.run { + themeSearchCardView(searchCardWrapper) + colorMaterialButtonPrimaryOutlined(sendCopyBtn) + colorMaterialButtonPrimaryBorderless(sharesListInternalShowAll) + } +} +``` + +Use `apply {}` when configuring and returning the receiver: + +```kotlin +ShareeListAdapter(fileActivity!!, ArrayList(), this, userId, user, viewThemeUtils, encrypted, type) + .apply { setHasStableIds(true) } +``` + +## 3. `switch` → `when` / `filter` + `partition` + +Collapse a `switch` that sorts items into buckets into a declarative pipeline with a +constant `Set`. + +```kotlin +// BEFORE: for-loop with switch(shareType) adding to internalShares / externalShares + +// AFTER +private val externalShareTypes = setOf( + ShareType.PUBLIC_LINK, ShareType.FEDERATED_GROUP, ShareType.FEDERATED, ShareType.EMAIL +) + +val (external, internal) = shares + .filter { it.shareType != null } + .partition { it.shareType in externalShareTypes } +``` + +## 4. Extension Functions & KTX + +Import members directly and lean on AndroidX KTX instead of verbose Java utilities. + +| Java / verbose | Idiomatic Kotlin | +|---|---| +| `TextUtils.isEmpty(s)` | `s.isNullOrEmpty()` | +| `BundleExtensionsKt.getParcelableArgument(b, k, T.class)` | `b.getParcelableArgument(k, T::class.java)` | +| `for (int i = 0; i < vg.getChildCount(); i++)` | `for (i in 0..` / `// endregion` (lifecycle, +private methods, overrides, companion) aids IDE folding. This is IDE structure, not a +decorative divider. Match the surrounding file's existing style; do not introduce ASCII +banner comments (`// ==== ====`), which the project forbids. diff --git a/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md new file mode 100644 index 000000000000..fd172f986144 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md @@ -0,0 +1,120 @@ +# Concurrency: Java Threads → Coroutines & `lifecycleScope` + +Replace `new Thread`, `AsyncTask`, executors, and `runOnUiThread`/`Handler.post` +callback-passing with structured coroutines scoped to the component lifecycle. This is +behaviour-preserving *and* fixes leaks: `lifecycleScope`/`viewModelScope` cancel +automatically when the owner is destroyed, so no work touches a dead view. + +## Choose the Right Scope + +| Context | Scope | +|---|---| +| `Fragment` (UI work tied to view) | `viewLifecycleOwner.lifecycleScope` (preferred) or `lifecycleScope` | +| `Activity` | `lifecycleScope` | +| `ViewModel` | `viewModelScope` | +| No lifecycle owner | inject a `CoroutineScope` / use a repository suspend fun | + +Use `Dispatchers.IO` for disk/network/DB, `Dispatchers.Main` (or `withContext(Main)`) to +touch views. + +## `new Thread { ... runOnUiThread(...) }` → `lifecycleScope.launch` + `withContext` + +```kotlin +// BEFORE +private void fetchE2EECounter(Runnable onComplete) { + new Thread(() -> { + try { + OwnCloudClient client = clientFactory.create(user); + Object metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, ctx); + if (metadata instanceof DecryptedFolderMetadataFile m) { + file.setE2eCounter(m.getMetadata().getCounter()); + fileDataStorageManager.saveFile(file); + } + } catch (Exception e) { Log_OC.e(TAG, "..." + e.getMessage()); } + Activity a = getActivity(); + if (a != null) a.runOnUiThread(onComplete); + }).start(); +} + +// AFTER: background work is a suspend fun on IO; the UI reaction runs on Main +private suspend fun fetchE2EECounter(): Boolean = withContext(Dispatchers.IO) { + return@withContext try { + val context = requireContext() + val client = clientFactory.create(user) + val metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, context) + if (metadata is DecryptedFolderMetadataFile) { + file?.setE2eCounter(metadata.metadata.counter) + fileDataStorageManager?.saveFile(file) + } + true + } catch (e: Exception) { + Log_OC.e(TAG, "Error refreshing E2E counter: " + e.message) + false + } +} + +// caller +lifecycleScope.launch { + val ok = fetchE2EECounter() + if (!ok) return@launch + withContext(Dispatchers.Main) { /* update views */ } +} +``` + +Notes: +- The old code returned nothing and drove the UI via a `Runnable`. The idiomatic version + returns a `Boolean` result and lets the caller decide — same observable outcome, no + callback threading. +- `getActivity() != null` guard becomes `lifecycleScope` cancellation + `binding?`/`?: + return` guards. Preserve any "is the view still alive?" check as a `binding == null` + guard inside `launch`. + +## Callback-Pair API → `suspend` Result + +Repositories that took `onSuccess`/`onError` lambdas become `suspend` functions returning +a result, awaited inside a coroutine. + +```kotlin +// BEFORE +shareRepository.fetchSharees(remotePath, onSuccess = { ... }, onError = { ... }); + +// AFTER +lifecycleScope.launch { + val result = shareRepository.fetchSharees(remotePath) + if (binding == null) return@launch + if (result) { refreshSharesFromDB(); stopLoadingAnimationAndShowShareContainer() } + else { stopLoadingAnimationAndShowShareContainer(); showError() } +} +``` + +## Moving DB/IO off the Main Thread + +Wrap the blocking DB read in `withContext(Dispatchers.IO)` and hop back to `Main` to touch +adapters/views. This can be a *behaviour improvement* (fewer main-thread stalls) — flag it +to the developer if the Java version was doing DB work on the main thread synchronously, +since timing changes can be observable (e.g. tests that assumed synchronous population). + +```kotlin +private suspend fun loadAndPartitionShares(): Pair, List> = + withContext(Dispatchers.IO) { + val shares = fileDataStorageManager?.getSharesWithForAFile(file?.remotePath, user?.accountName) + ?: emptyList() + val (external, internal) = shares.filter { it.shareType != null } + .partition { it.shareType in externalShareTypes } + internal to external + } +``` + +## RxJava Present? + +If the file uses `io.reactivex.*`/`rx.*`, load +[frameworks/RXJAVA.md](frameworks/RXJAVA.md) for the reactive-type → Flow/coroutine +mapping instead of hand-rolling. + +## Pitfalls + +- Never launch on `GlobalScope` — it outlives the screen and leaks. +- Keep `try/catch` semantics: coroutine cancellation throws `CancellationException`; + rethrow it (don't swallow in a broad `catch (e: Exception)` that logs and continues) or + catch `Exception` only around the real work, not around the whole `launch`. +- Preserve exception-to-UI mapping exactly (same snackbar, same log tag/message). diff --git a/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md b/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md new file mode 100644 index 000000000000..908fb2390a7b --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md @@ -0,0 +1,352 @@ +# Conversion Methodology + +You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is +to convert provided Java code into **idiomatic Kotlin**, preserving behaviour while +improving readability, safety and maintainability. + +## The 4-Step Precognition Process + +Before emitting any code, run through the provided Java input and perform these 4 steps +of thinking. After each step, output the code as you have it after that step's +transformation has been applied. + +### Step 1: Faithful 1:1 Translation + +Convert the Java code 1 to 1 into Kotlin, prioritising faithfulness to the original +Java semantics, to replicate the Java code's functionality and logic exactly. + +**Rules:** +- Java classes that are implicitly open MUST be converted as Kotlin classes that are + explicitly `open`, using the `open` keyword. +- To convert Java constructors that inject into fields, use the Kotlin primary + constructor. Any further logic within the Java constructor can be replicated with the + Kotlin secondary constructor. + +### Step 2: Nullability & Mutability + +Check that mutability and nullability are correctly expressed in your Kotlin conversion. +Only express types as non-null where you are sure that it can never be null, inferred +from the original Java. Use `val` instead of `var` where you see variables that are +never modified. + +**Rules:** +- If you see a logical assertion that a value is not null (e.g., `Objects.requireNonNull`), + this shows that the author has considered that the value can never be null. Use a + non-null type in this case, and remove the logical assertion. +- In all other cases, preserve the fact that types can be null in Java by using the + Kotlin nullable version of that type. + +### Step 3: Collection Type Conversion + +Convert datatypes like collections from their Java variants to the Kotlin variants. + +**Rules:** +- For Java collections like `List` that are mutable by default, always use the Kotlin + `MutableList`, unless you see explicitly that the Java code uses an immutable wrapper + (e.g., `Collections.unmodifiableList()`) — in this case, use the Kotlin `List` (and + so on for other collections like `Set`, `Map` etc.) + +### Step 4: Idiomatic Transformations + +Introduce syntactic transformations to make the output truly idiomatic. + +**Rules:** +- Where getters and setters are defined as methods in Java, use the Kotlin syntax to + replace these methods with a more idiomatic version. +- Lambdas should be used where they can simplify code complexity while replicating the + exact behaviour of the previous code. + +## The 5 Invariants + +In each stage of your chain of thought, the following invariants must hold. + +**Invariant 1:** No new side-effects or behaviour. + +**Invariant 2:** Preserve all annotations and targets exactly. +- Annotations must target the backing field in Kotlin where they targeted the field in + Java. Use annotation site targets: `@field:`, `@get:`, `@set:`, `@param:`. + +**Invariant 3:** Preserve the package declaration and all imports. +- Carry forwards every single import, adding no new imports. Only remove imports where + they would shadow Kotlin names (e.g., `java.util.List` shadows Kotlin's `List`). + +**Invariant 4:** Preserve all Javadoc comments. +- In step 1, convert any Javadoc comments to KDoc comments. +- In all other steps, carry forwards the KDoc comments, preserving information where + structure changes. + +**Invariant 5:** Ensure the output result is in Kotlin. +- The emitted code must be syntactically valid Kotlin. + +**After each step**, go through each of these invariants, listing the ones that no +longer hold. If any exist, revert to the previous step and recalculate from there. + +--- + +## Example 1: Utility Class with Nullability + +### Java Input + +```java +package com.acme.util; + +import java.time.LocalDate; +import java.util.Objects; + +/** +* Utility for printing a greeting that includes the current date. +*/ +public class DateGreeter { + /** + * Prints a greeting for the given name and the current date. + * + * @param name an optional name; if {@code null}, the greeting uses {@code "Guest"} + */ + public static void greet(String name) { + String who = (name != null) ? name : "Guest"; + System.out.println("Hello, " + who + " - today is " + LocalDate.now()); + } +} +``` + +### Conversion Walkthrough + +**Step 1** — Faithful translation. The DateGreeter class is implicitly open, the +`greet` method is static → companion object. Javadoc converted to KDoc. + +```kotlin +package com.acme.util + +import java.time.LocalDate +import java.util.Objects + +/** +* Utility for printing a greeting that includes the current date. +*/ +open class DateGreeter { + companion object { + /** + * Prints a greeting for the given [name] and the current date. + * + * @param name an optional name; if `null`, the greeting uses `"Guest"` + */ + fun greet(name: String?) { + var who = if (name != null) name else "Guest" + println("Hello, " + who + " - today is " + LocalDate.now()) + } + } +} +``` + +Invariants check: All OK. + +**Step 2** — The `String?` is correct since the author checks for null. The `who` +variable is only read, so change `var` → `val`. + +```kotlin +// Same as above but with: +val who = if (name != null) name else "Guest" +``` + +Invariants check: All OK. + +**Step 3** — No collections in this code. No changes. + +Invariants check: All OK. + +**Step 4** — The `greet` function is not tied to any state of DateGreeter, so move it +to a top-level function. Use string templates and Elvis operator. + +```kotlin +package com.acme.util + +import java.time.LocalDate +import java.util.Objects + +/** +* Prints a greeting for the given [name] and the current date. +* +* @param name an optional name; if `null`, the greeting uses `"Guest"` +*/ +fun greet(name: String?) { + println("Hello, ${name ?: "Guest"} - today is ${LocalDate.now()}") +} +``` + +Invariants check: All OK. + +--- + +## Example 2: Domain Model with Annotations + +### Java Input + +```java +package com.acme.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import java.util.Objects; + +/** +* Domain model for a user with a required identifier and an optional nickname. +*

+* The {@code id} is serialized as {@code "id"} and is required. +* The {@code nickname} may be absent. +*/ +public class User { + /** + * Stable, non-null identifier serialized as {@code "id"}. + */ + @JsonProperty("id") + private final String id; + + /** + * Optional nickname for display purposes. + */ + @Nullable + private String nickname; + + /** + * Creates a user with the given non-null identifier. + * + * @param id required identifier for the user + * @throws NullPointerException if {@code id} is null + */ + public User(String id) { + this.id = Objects.requireNonNull(id, "id"); + } + + /** + * Returns the identifier serialized as {@code "id"}. + * + * @return the user id + */ + @JsonProperty("id") + public String getId() { + return id; + } + + /** + * Returns the optional nickname. + * + * @return the nickname or {@code null} if absent + */ + @Nullable + public String getNickname() { + return nickname; + } + + /** + * Sets the optional nickname. + * + * @param nickname the nickname or {@code null} to clear it + */ + public void setNickname(@Nullable String nickname) { + this.nickname = nickname; + } +} +``` + +### Conversion Walkthrough + +**Step 1** — Faithful translation. Class is implicitly open → `open class`. +`@JsonProperty("id")` on the field → `@field:JsonProperty("id")`. +`@JsonProperty("id")` on the getter → `@get:JsonProperty("id")` when converted to +property later. Keep explicit getters/setters at this step for faithfulness. + +```kotlin +package com.acme.model + +import com.fasterxml.jackson.annotation.JsonProperty +import javax.annotation.Nullable +import java.util.Objects + +/** +* Domain model for a user with a required identifier and an optional nickname. +* +* The `id` is serialized as `"id"` and is required. +* The `nickname` may be absent. +*/ +open class User { + + /** + * Stable, non-null identifier serialized as `"id"`. + */ + @field:JsonProperty("id") + private val id: String + + /** + * Optional nickname for display purposes. + */ + @field:Nullable + private var nickname: String? = null + + /** + * Creates a user with the given non-null identifier. + * + * @param id required identifier + * @throws NullPointerException if `id` is `null` + */ + constructor(id: String) { + this.id = Objects.requireNonNull(id, "id") + } + + @get:JsonProperty("id") + fun getId(): String { return id } + + @Nullable + fun getNickname(): String? { return nickname } + + fun setNickname(@Nullable nickname: String?) { this.nickname = nickname } +} +``` + +Invariants check: All OK. + +**Step 2** — `id` is non-null by design (`Objects.requireNonNull` enforces it). +`nickname` is nullable (`@Nullable`). No val/var changes needed beyond what's already +done. Code unchanged. + +Invariants check: All OK. + +**Step 3** — No collections. No changes. + +Invariants check: All OK. + +**Step 4** — Idiomatic Kotlin: +1. Primary constructor with `id` as a `val` property. Apply both `@field:JsonProperty` + and `@get:JsonProperty` to match both Java annotation targets. +2. Convert `nickname` getter/setter → Kotlin property with `@field:Nullable` and + `@get:Nullable`. +3. Drop `Objects.requireNonNull` — Kotlin's type system enforces non-null. +4. Preserve all imports even if now unused (invariant 3). + +```kotlin +package com.acme.model + +import com.fasterxml.jackson.annotation.JsonProperty +import javax.annotation.Nullable +import java.util.Objects + +/** +* Domain model for a user with a required identifier and an optional nickname. +* +* The `id` is serialized as `"id"` and is required. +* The `nickname` may be absent. +* +* @property id stable, non-null identifier serialized as `"id"` +* @property nickname optional nickname for display purposes; may be `null` if not set +*/ +open class User( + @field:JsonProperty("id") + @get:JsonProperty("id") + val id: String +) { + @field:Nullable + @get:Nullable + var nickname: String? = null +} +``` + +Invariants check: All OK. diff --git a/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md b/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md new file mode 100644 index 000000000000..a23743ffc6ed --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md @@ -0,0 +1,108 @@ +# Fail Fast: Guard Clauses Over Nested `if`/`else` + +The IDE preserves Java's nested-`if` pyramids. Invert them into guard clauses that return +(or throw) early, leaving the happy path at the lowest indentation. Behaviour is +identical — the branches are the same, only the shape changes. + +## Precondition Checks → `require` / `requireNotNull` + +`requireNotNull` returns the smart-cast non-null value AND throws +`IllegalArgumentException` with the message — exactly matching the Java `if (x == null) +throw new IllegalArgumentException(...)`. + +```kotlin +// BEFORE +if (file == null) throw IllegalArgumentException("File may not be null"); +if (user == null) throw IllegalArgumentException("Account may not be null"); +fileActivity = (FileActivity) getActivity(); +if (fileActivity == null) throw IllegalArgumentException("FileActivity may not be null"); + +// AFTER +fileActivity = activity as? FileActivity +requireNotNull(file) { "File may not be null" } +requireNotNull(user) { "Account may not be null" } +requireNotNull(fileActivity) { "FileActivity may not be null" } +``` + +Use `require(condition) { msg }` for boolean preconditions: + +```kotlin +require(activity is FileActivity) { "Calling activity must be of type FileActivity" } +``` + +`check`/`checkNotNull` are the `IllegalStateException` equivalents — use them when the Java +threw `IllegalStateException`. Match the original exception type; that is observable +behaviour. + +## Early Return Over Nested Success Path + +```kotlin +// BEFORE +private void checkShareViaUser() { + if (!MDMConfig.INSTANCE.shareViaUser(requireContext())) { + binding.searchContainer.setVisibility(View.GONE); + } +} + +// AFTER +private fun checkShareViaUser() { + if (shareViaUser(requireContext())) return + binding?.searchContainer?.visibility = View.GONE +} +``` + +## Deeply Nested `if`/`else` → Sequential Guards + +The most valuable transformation. A cursor-handling method nested three levels deep +becomes a flat sequence of guard clauses, each handling one failure and returning. + +```kotlin +// BEFORE: if (cursor != null) { if (moveToFirst()) { if (columnIndex != -1) {...} else ... } else ... } else ... + +// AFTER +private fun handleContactResult(contactUri: Uri) { + val cursor = fileActivity?.contentResolver?.query(contactUri, projection, null, null, null) + if (cursor == null) { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed) + Log_OC.e(TAG, "Failed to pick email address as Cursor is null.") + return + } + if (!cursor.moveToFirst()) { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed) + Log_OC.e(TAG, "Failed to pick email address as no Email found.") + return + } + val columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + if (columnIndex == -1) { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed) + Log_OC.e(TAG, "Failed to pick email address.") + cursor.close() + return + } + val email = cursor.getString(columnIndex) + // ... happy path at base indentation + cursor.close() +} +``` + +Watch the cleanup: if the Java relied on falling through to a single `cursor.close()`, +each early return must still close it (or wrap in `use {}`). Missing that changes +behaviour (resource leak) — verify it. + +## Nullable-Guard Idioms + +```kotlin +val activity = fileActivity ?: return +val clientRepository = activity.clientRepository ?: return +val remotePath = file?.remotePath ?: return +``` + +Each `?: return` collapses one Java `if (x == null) return;`. Chain them at the top of the +function so the body works with non-null smart-cast locals. + +## When NOT to Invert + +- Do not turn a genuine two-branch decision (both branches do real work) into a guard if + it obscures the symmetry — a `when`/`if-else` expression is clearer there. +- Do not change the *order* of side-effects while inverting; the snackbar/log calls above + must fire in the same cases as before. diff --git a/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md b/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md new file mode 100644 index 000000000000..9de639d65ec2 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md @@ -0,0 +1,358 @@ +# Known Issues and Common Pitfalls + +A reference of common issues encountered during Java-to-Kotlin conversion, with solutions. + +### Kotlin Keyword Conflicts + +Java identifiers that are reserved keywords in Kotlin will cause compilation errors after conversion. + +**Affected keywords:** `when`, `in`, `is`, `object`, `fun`, `val`, `var`, `typealias`, `as` + +**Solution:** Backtick-escape them in Kotlin: + +```java +// Java +public void when(String event) { ... } +public boolean in(List items) { ... } +``` + +```kotlin +// Kotlin — backtick-escaped +fun `when`(event: String) { ... } +fun `in`(items: List): Boolean { ... } +``` + +When the API is internal (not exposed to other modules), prefer renaming the identifier to a non-keyword alternative instead of using backticks. For example, rename `when` to `onEvent` or `in` to `contains`. + +### SAM Conversion Ambiguity + +When a Java method has overloads that each accept a different SAM (Single Abstract Method) interface, Kotlin's trailing lambda syntax becomes ambiguous. The compiler cannot determine which SAM interface the lambda should implement. + +```java +// Java — overloaded method accepting different SAM types +public class TaskExecutor { + void submit(Runnable task) { ... } + void submit(Callable task) { ... } +} +``` + +```kotlin +// Kotlin — WRONG: ambiguous, won't compile +executor.submit { doWork() } + +// Kotlin — CORRECT: explicit SAM constructor +executor.submit(Runnable { doWork() }) +executor.submit(Callable { computeResult() }) +``` + +Use explicit SAM constructor calls whenever there are overloaded methods accepting different functional interfaces. + +### Platform Types + +Java types without nullability annotations (`@Nullable`, `@NotNull`, `@NonNull`) become "platform types" (`T!`) in Kotlin. Platform types bypass Kotlin's null-safety system — they are neither nullable nor non-null, and null checks are deferred to runtime. + +```java +// Java — no nullability annotations +public String getName() { return name; } +public List getItems() { return items; } +``` + +```kotlin +// Kotlin — BAD: platform types left in converted code +val name = obj.name // inferred as String! — unsafe +val items = obj.items // inferred as List! — unsafe + +// Kotlin — GOOD: explicit nullability based on code analysis +val name: String = obj.name // if provably non-null +val name: String? = obj.name // if could be null +val items: List = obj.items // if neither list nor elements are null +``` + +Always add explicit type declarations to eliminate platform types. Analyze the Java source code, documentation, and call sites to determine the correct nullability. + +### @JvmStatic / @JvmField / @JvmOverloads + +When converted Kotlin code is still called from Java, use JVM interop annotations to maintain a clean Java API: + +**`@JvmStatic`** — Makes companion object functions accessible as static methods from Java: + +```kotlin +class Config { + companion object { + @JvmStatic + fun getInstance(): Config = ... + } +} +``` + +```java +// Java callers can use: Config.getInstance() +// Without @JvmStatic they would need: Config.Companion.getInstance() +``` + +**`@JvmField`** — Exposes a property as a direct field rather than through getter/setter: + +```kotlin +class Constants { + companion object { + @JvmField + val DEFAULT_TIMEOUT = 30_000L + } +} +``` + +```java +// Java callers can use: Constants.DEFAULT_TIMEOUT +// Without @JvmField they would need: Constants.Companion.getDEFAULT_TIMEOUT() +``` + +**`@JvmOverloads`** — Generates Java overloads for functions with default parameters: + +```kotlin +@JvmOverloads +fun connect(host: String, port: Int = 443, secure: Boolean = true) { ... } +``` + +```java +// Java sees three overloads: +// connect(String host) +// connect(String host, int port) +// connect(String host, int port, boolean secure) +``` + +### Checked Exceptions + +Kotlin does not have checked exceptions. When Kotlin code is called from Java, the Java compiler will not know about thrown exceptions unless annotated with `@Throws`: + +```kotlin +// Without @Throws, Java callers cannot catch IOException in a catch block +// (the Java compiler will say "exception is never thrown in the corresponding try block") + +@Throws(IOException::class) +fun readFile(path: String): String { + return File(path).readText() +} +``` + +Add `@Throws` to every Kotlin function that throws checked exceptions and is called from Java code. + +### Wildcard Generics + +Java wildcard types map to Kotlin's variance annotations: + +| Java | Kotlin | Description | +|------|--------|-------------| +| `? extends T` | `out T` | Covariance (producer) | +| `? super T` | `in T` | Contravariance (consumer) | +| Raw type `List` | `List` | Add explicit type parameter | + +```java +// Java +public void process(List numbers) { ... } +public void addAll(List target) { ... } +public void legacy(List items) { ... } // raw type +``` + +```kotlin +// Kotlin +fun process(numbers: List) { ... } +fun addAll(target: MutableList) { ... } +fun legacy(items: List) { ... } // explicit type parameter +``` + +For raw types, analyze the code to determine the most specific type parameter rather than defaulting to `Any?`. + +### Static Members + +Java's `static` keyword has no direct equivalent in Kotlin. Use the following mappings: + +**Static methods** — Use companion object functions, or top-level functions if they don't need class state: + +```java +// Java +public class StringUtils { + public static String capitalize(String s) { ... } +} +``` + +```kotlin +// Kotlin — top-level function (preferred when no class state needed) +fun capitalize(s: String): String { ... } + +// Kotlin — companion object (when logically tied to the class) +class StringUtils { + companion object { + fun capitalize(s: String): String { ... } + } +} +``` + +**Static constants** — Use `const val` for compile-time constants (primitives and String), `val` for object constants: + +```kotlin +class HttpStatus { + companion object { + const val OK = 200 // primitive — const val + const val NOT_FOUND_MESSAGE = "Not Found" // String — const val + val DEFAULT_HEADERS = mapOf("Accept" to "application/json") // object — val + } +} +``` + +**Static initializers** — Use companion object `init {}` block or top-level code: + +```kotlin +class Registry { + companion object { + private val handlers = mutableMapOf() + init { + handlers["default"] = DefaultHandler() + } + } +} +``` + +### Synchronized Blocks + +Java's `synchronized` constructs map to Kotlin as follows: + +**Synchronized blocks** — Use Kotlin's `synchronized()` function: + +```java +// Java +synchronized (lock) { + sharedState.update(); +} +``` + +```kotlin +// Kotlin +synchronized(lock) { + sharedState.update() +} +``` + +**Synchronized methods** — Use the `@Synchronized` annotation: + +```java +// Java +public synchronized void update() { ... } +``` + +```kotlin +// Kotlin +@Synchronized +fun update() { ... } +``` + +### Anonymous Inner Classes + +**Single Abstract Method (SAM) interfaces** — Convert to lambda syntax: + +```java +// Java +executor.submit(new Runnable() { + @Override + public void run() { + doWork(); + } +}); +``` + +```kotlin +// Kotlin +executor.submit(Runnable { doWork() }) +``` + +**Multiple methods or abstract classes** — Use `object` expression: + +```java +// Java +view.addListener(new ViewListener() { + @Override + public void onOpen() { ... } + @Override + public void onClose() { ... } +}); +``` + +```kotlin +// Kotlin +view.addListener(object : ViewListener { + override fun onOpen() { ... } + override fun onClose() { ... } +}) +``` + +### Array Handling + +Java arrays map to Kotlin types as follows: + +| Java | Kotlin | Notes | +|------|--------|-------| +| `String[]` | `Array` | Reference type arrays | +| `int[]` | `IntArray` | Primitive array (not `Array`) | +| `long[]` | `LongArray` | Primitive array | +| `double[]` | `DoubleArray` | Primitive array | +| `boolean[]` | `BooleanArray` | Primitive array | +| `Object[]` | `Array` | | +| `new int[10]` | `IntArray(10)` | Array creation | +| `new String[10]` | `arrayOfNulls(10)` | Nullable element array | +| `String... args` | `vararg args: String` | Varargs parameter | + +Using `Array` instead of `IntArray` causes boxing overhead — always use the specialized primitive array types. + +### Ternary Operator + +Kotlin has no ternary operator. Use `if`/`else` as an expression: + +```java +// Java +String label = (count > 0) ? "Items: " + count : "Empty"; +``` + +```kotlin +// Kotlin +val label = if (count > 0) "Items: $count" else "Empty" +``` + +### instanceof + +Java's `instanceof` maps to Kotlin's `is` keyword. Kotlin supports smart casting, so an explicit cast after an `is` check is unnecessary: + +```java +// Java +if (shape instanceof Circle) { + Circle circle = (Circle) shape; + double area = circle.getArea(); +} +``` + +```kotlin +// Kotlin — smart cast, no explicit cast needed +if (shape is Circle) { + val area = shape.area // shape is automatically cast to Circle +} +``` + +### try-with-resources + +Java's try-with-resources maps to Kotlin's `.use {}` extension function: + +```java +// Java +try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + String line = reader.readLine(); + process(line); +} +``` + +```kotlin +// Kotlin +BufferedReader(FileReader(path)).use { reader -> + val line = reader.readLine() + process(line) +} +``` + +The `.use {}` function works on any `Closeable` or `AutoCloseable` instance and guarantees the resource is closed even if an exception is thrown. diff --git a/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md b/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md new file mode 100644 index 000000000000..39f779079e82 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md @@ -0,0 +1,74 @@ +# Project Conventions (Nextcloud Android) + +Apply these during Step 2 so the converted file passes review and the quality gates. They +are enforced by `spotlessKotlinCheck`, `detekt`, `lint`, and `spotbugsGplayDebug`. + +## SPDX Header (every new/renamed file) + +The IDE keeps the old license block. Replace it with the current template. The year is the +year the Kotlin file is created. New contributions are `AGPL-3.0-or-later`; keep +`OR GPL-2.0-only` only if the original file carried it. + +```kotlin +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +``` + +If the developer wants personal attribution (as in the real conversion), the form +`SPDX-FileCopyrightText: ` is also used — match what the developer +asks for; default to the "Nextcloud GmbH and Nextcloud contributors" line. + +## Structural Rules + +- **≤300 lines per file.** If decomposition pushes past it, split responsibilities into + separate files/collaborators and tell the developer. A file/class-level + `@Suppress("LargeClass", "TooManyFunctions")` is a last resort for legacy god-classes. +- **≤120 columns per line.** +- **One top-level type per file.** Extract models, states, sealed classes, and listener + interfaces into their own files rather than nesting many types in one. +- **Exactly one trailing newline** at end of file. + +## No Magic Numbers / Hardcoded Resources + +- Extract literals into named `const val` in a `companion object` + (`MIN_SHOW_ALL_VISIBLE_ITEM_COUNT = 3`, `INTERNAL_LINK_PATH_PRETTY = "/f/"`). +- Strings, colors, dimens come from resources (`R.string.*`, `R.dimen.*`), never inline. +- Only `app/src/main/res/values/strings.xml` may be edited for strings; never touch + `values-*` translation folders. + +## Comments & Naming + +- No decorative divider comments (`// ==== ====`, `// ---- Title ----`). `// region` / + `// endregion` for IDE folding is allowed and should match the file's existing style. +- Prefer self-explanatory names over per-function KDoc. Preserve genuinely informative + Javadoc as KDoc (invariant 4); drop noise. +- Do not use multiple boolean flags to model state — use an `enum`/sealed class. + +## Modern Java Interop + +When the file still has Java callers, keep the Java-facing API clean: +`@JvmStatic` for factory/companion functions, `@JvmField` for exposed constants, +`@JvmOverloads` for defaulted params, `@Throws` for checked exceptions. See +[KNOWN-ISSUES.md](KNOWN-ISSUES.md). + +## Git & Commits (developer-driven) + +- Preserve history: the rename `git mv Foo.java Foo.kt` should be a **separate commit** + from the content change so `git blame` follows through. +- Conventional Commits (`refactor(sharing): convert FileDetailSharingFragment to Kotlin`). +- Every AI-assisted commit needs an `Assisted-by: :` trailer. +- Only the human contributor adds `Signed-off-by` (DCO). You must never add it, and never + open PRs/issues autonomously (AI policy). + +## Quality Gate + +```bash +./gradlew spotlessKotlinCheck detektGplayDebug lintGplayDebug spotbugsGplayDebug \ + jacocoTestGplayDebugUnitTest +``` + +Fix every finding in the files you changed before declaring done. diff --git a/.claude/skills/android-java-to-kotlin/references/TESTING.md b/.claude/skills/android-java-to-kotlin/references/TESTING.md new file mode 100644 index 000000000000..20f313683e0e --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/TESTING.md @@ -0,0 +1,107 @@ +# Behaviour-Locking Tests (Mandatory Step) + +A conversion is not done until a test proves behaviour did not change. If the Java class +already had tests, they are your primary safety net — run them and keep them green. If it +had none, you must add a **characterization test** that pins the current behaviour so the +refactor is provably safe. + +## Decision Tree + +```dot +digraph test_choice { + rankdir=LR; + "What are you locking?" -> "Pure logic (no Android SDK)" [label="fn returns a value"]; + "What are you locking?" -> "Component behaviour" [label="Fragment/Activity/DB/View"]; + "Pure logic (no Android SDK)" -> "JVM unit test app/src/test/ (JUnit4 + mockito-kotlin)"; + "Component behaviour" -> "Robolectric unit test (if project uses it)"; + "Component behaviour" -> "Instrumented test app/src/androidTest/ extends base IT class"; +} +``` + +**Prefer the seams you just created.** The idiomatic pass extracts pure functions +(link builders, permission math, partitioning, `isReshareForbidden`) specifically so they +can be unit-tested fast without a device. Test those first; they give the most behaviour +coverage per second. + +## 1. JVM Unit Tests — Pure Functions + +Location: `app/src/test/`. Fast, no emulator. Command: +`./gradlew jacocoTestGplayDebugUnitTest`. + +Mark the function `@VisibleForTesting internal` so the test module can reach it while it +stays out of the public API. + +```kotlin +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment + +import com.owncloud.android.lib.resources.status.OCCapability +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class FileDetailSharingFragmentTest { + + @Test + fun `internal link uses pretty path when modRewrite is on`() { + val user = mock { whenever(it.server.uri).thenReturn(URI("https://cloud.example")) } + val file = mock { whenever(it.localId).thenReturn(42L) } + val caps = mock { whenever(it.modRewriteWorking.isTrue).thenReturn(true) } + + val link = FileDetailSharingFragment().createInternalLink(user, file, caps) + + assertEquals("https://cloud.example/f/42", link) + } + + @Test + fun `internal link falls back to index php path when modRewrite is off`() { + // ... same setup with modRewriteWorking.isTrue == false + // assertEquals("https://cloud.example/index.php/f/42", link) + } +} +``` + +The point: run this test's logic against **both** the pre-conversion and post-conversion +code paths mentally (or, if the Java is still on disk, literally) and confirm the assertion +holds for both. That is what "behaviour must not change" means concretely. + +## 2. Instrumented Tests — Component Behaviour + +Location: `app/src/androidTest/`. For Fragment/Activity/DB behaviour that needs the Android +runtime. Extend the project's base test class (`AbstractOnServerIT` when server +communication is required) and follow its conventions (separate test user, etc.). + +```bash +./gradlew createGplayDebugCoverageReport -Pcoverage=true \ + -Pandroid.testInstrumentationRunnerArguments.class=com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT +``` + +Use `@VisibleForTesting` hooks the original exposed (e.g. a `search(query)` or +`showSharingMenuActionSheet(share)` method) to drive and assert UI state, exactly as the +existing IT suite does. Do not weaken visibility further than the Java original did. + +## 3. What to Assert + +Lock the observable contract, not the implementation: +- Same return values / thrown exception types for the same inputs (including the + `require`/`requireNotNull` messages if callers depend on them). +- Same Bundle keys, intent extras, and `newInstance` argument wiring. +- Same branch outcomes (e.g. "reshare forbidden when FEDERATED", "show-all button visible + only when > 3 shares"). +- For coroutine conversions, assert the end state, and use the project's test dispatcher / + `runTest` so async work is deterministic — never assert on wall-clock timing. + +## 4. Guardrails + +- Every new test file gets the SPDX header and ends with exactly one trailing newline. +- Do not modify unrelated tests to make yours pass. +- Report actual test output. If a test reveals the conversion changed behaviour, fix the + conversion — do not adjust the test to match the drift. +- If a genuine pre-existing bug is uncovered, surface it to the developer separately; do + not fold a behaviour change into the conversion. diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md new file mode 100644 index 000000000000..fe3636b92987 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md @@ -0,0 +1,160 @@ +# Dagger / Hilt Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `dagger.*` or +`dagger.hilt.*`. This covers Dagger 2, Hilt for Android, and Hilt Jetpack integrations. + +## Key Rules + +### 1. @Inject constructor syntax + +Kotlin places `@Inject` before the `constructor` keyword in the primary constructor: + +```kotlin +class Foo @Inject constructor(private val bar: Bar) +``` + +### 2. @Module classes with @Provides methods + +Keep `@Provides` methods `open`, or use `object` for modules that contain only +`@JvmStatic` provides methods (companion object pattern): + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build() +} +``` + +### 3. @Binds abstract methods + +`@Binds` methods work in abstract classes exactly as in Java. Convert the abstract +class directly — no special Kotlin considerations. + +### 4. Hilt Android annotations + +`@HiltAndroidApp`, `@AndroidEntryPoint`, `@HiltViewModel` — preserve these exactly +on Application, Activity, Fragment, and ViewModel classes. + +### 5. Scoping annotations + +`@Singleton`, `@ActivityScoped`, `@ViewModelScoped`, `@FragmentScoped` — preserve +exactly. No annotation site target is needed. + +### 6. @AssistedInject / @AssistedFactory + +`@AssistedInject` replaces `@Inject` on the constructor. `@Assisted` parameters +appear alongside regular injected parameters in the primary constructor: + +```kotlin +class PlayerViewModel @AssistedInject constructor( + @Assisted private val playerId: String, + private val repository: PlayerRepository +) : ViewModel() +``` + +### 7. @Component / @Subcomponent interfaces + +Convert directly to Kotlin interfaces. Dagger's annotation processing works +identically with Kotlin interfaces via kapt or KSP. + +--- + +## Examples + +### Example 1: Hilt ViewModel with @Inject Constructor and a @Module + +**Java:** + +```java +package com.acme.feature; + +import androidx.lifecycle.ViewModel; +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.android.lifecycle.HiltViewModel; +import dagger.hilt.components.SingletonComponent; +import javax.inject.Inject; +import javax.inject.Singleton; + +@HiltViewModel +public class UserProfileViewModel extends ViewModel { + + private final UserRepository userRepository; + private final AnalyticsTracker analyticsTracker; + + @Inject + public UserProfileViewModel(UserRepository userRepository, AnalyticsTracker analyticsTracker) { + this.userRepository = userRepository; + this.analyticsTracker = analyticsTracker; + } + + public LiveData getUser(String userId) { + analyticsTracker.trackProfileView(userId); + return userRepository.getUser(userId); + } +} + +@Module +@InstallIn(SingletonComponent.class) +public class AnalyticsModule { + + @Provides + @Singleton + public AnalyticsTracker provideAnalyticsTracker(Application app) { + return new AnalyticsTracker(app); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.feature + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject +import javax.inject.Singleton + +@HiltViewModel +class UserProfileViewModel @Inject constructor( + private val userRepository: UserRepository, + private val analyticsTracker: AnalyticsTracker +) : ViewModel() { + + fun getUser(userId: String): LiveData { + analyticsTracker.trackProfileView(userId) + return userRepository.getUser(userId) + } +} + +@Module +@InstallIn(SingletonComponent::class) +object AnalyticsModule { + + @Provides + @Singleton + fun provideAnalyticsTracker(app: Application): AnalyticsTracker { + return AnalyticsTracker(app) + } +} +``` + +Key changes: +- `@Inject` moves before the `constructor` keyword in the primary constructor. +- Constructor parameters become `private val` in the primary constructor. +- The module class becomes an `object` since it contains only static-like provides methods. +- `SingletonComponent.class` becomes `SingletonComponent::class` (Kotlin class reference). +- Java getter method `getUser` becomes a regular function `getUser` (no `get` prefix + convention change needed here since it takes a parameter). diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md new file mode 100644 index 000000000000..fb4a286e419b --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md @@ -0,0 +1,193 @@ +# JUnit / TestNG Conversion Guide + +## When This Applies + +Detected when imports match `org.junit.*` or `org.testng.*`. + +## Key Rules + +### 1. JUnit 4 to Kotlin (with JUnit 5) + +| JUnit 4 | Kotlin (JUnit 5 / kotlin.test) | +|---|---| +| `@Test` | `@Test` (from `kotlin.test` or `org.junit.jupiter.api`) | +| `@Before` | `@BeforeEach` (JUnit 5) or `@BeforeTest` (kotlin.test) | +| `@After` | `@AfterEach` (JUnit 5) or `@AfterTest` (kotlin.test) | +| `@BeforeClass` | `@BeforeAll` in companion object with `@JvmStatic` | +| `@AfterClass` | `@AfterAll` in companion object with `@JvmStatic` | +| `@RunWith` | `@ExtendWith` (JUnit 5) | +| `@Ignore` | `@Disabled` (JUnit 5) | +| `@Rule` / `@ClassRule` | `@ExtendWith` or `@RegisterExtension` | +| `Assert.assertEquals(expected, actual)` | `assertEquals(expected, actual)` (kotlin.test) | +| `Assert.assertTrue(condition)` | `assertTrue(condition)` (kotlin.test) | +| `@Test(expected = X.class)` | `assertFailsWith { }` (kotlin.test) or `assertThrows { }` (JUnit 5) | + +### 2. JUnit 5 stays mostly the same + +JUnit 5 annotations (`@Test`, `@BeforeEach`, `@AfterEach`, etc.) remain unchanged. +Focus on Kotlin idioms in the test body: + +- `assertThrows { code }` — uses reified generics, no `.class` needed. +- Test classes and methods do not need to be `public` — Kotlin's default visibility + is public, which satisfies JUnit's requirements. +- Test methods do not need `open` unless using a framework that subclasses the test + (e.g., certain Spring test configurations). + +### 3. TestNG to Kotlin + +| TestNG | Kotlin (JUnit 5) | +|---|---| +| `@Test` | `@Test` | +| `@BeforeMethod` | `@BeforeEach` | +| `@AfterMethod` | `@AfterEach` | +| `@BeforeClass` | `@BeforeAll` with `@JvmStatic` in companion object | +| `@AfterClass` | `@AfterAll` with `@JvmStatic` in companion object | +| `@DataProvider` | `@ParameterizedTest` + `@MethodSource` | + +### 4. Assertion style + +Prefer `kotlin.test` assertions (`assertEquals`, `assertTrue`, `assertFailsWith`) +for portability across test frameworks. They delegate to the underlying framework +at runtime. + +### 5. Backtick method names + +Kotlin allows backtick-quoted method names for readable test names: +```kotlin +@Test +fun `should return empty list when no users exist`() { ... } +``` + +--- + +## Example: JUnit 4 Test Class to Kotlin with JUnit 5 + +### Java Input + +```java +package com.acme.service; + +import org.junit.Before; +import org.junit.After; +import org.junit.Test; +import org.junit.BeforeClass; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for the UserService class. + */ +public class UserServiceTest { + + private static DatabaseConnection db; + private UserService userService; + + @BeforeClass + public static void setupDatabase() { + db = DatabaseConnection.create("test"); + } + + @Before + public void setUp() { + userService = new UserService(db); + } + + @After + public void tearDown() { + db.clearTestData(); + } + + @Test + public void testFindById() { + User user = userService.findById(1L); + assertNotNull(user); + assertEquals("Alice", user.getName()); + } + + @Test + public void testFindAllReturnsNonEmptyList() { + List users = userService.findAll(); + assertNotNull(users); + assertTrue(users.size() > 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindByIdWithNegativeIdThrows() { + userService.findById(-1L); + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.service + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for the UserService class. + */ +class UserServiceTest { + + companion object { + private lateinit var db: DatabaseConnection + + @BeforeAll + @JvmStatic + fun setupDatabase() { + db = DatabaseConnection.create("test") + } + } + + private lateinit var userService: UserService + + @BeforeEach + fun setUp() { + userService = UserService(db) + } + + @AfterEach + fun tearDown() { + db.clearTestData() + } + + @Test + fun `should find user by id`() { + val user = userService.findById(1L) + assertNotNull(user) + assertEquals("Alice", user.name) + } + + @Test + fun `should return non-empty list from findAll`() { + val users = userService.findAll() + assertNotNull(users) + assertTrue(users.isNotEmpty()) + } + + @Test + fun `should throw IllegalArgumentException for negative id`() { + assertFailsWith { + userService.findById(-1L) + } + } +} +``` + +**Key points:** +- JUnit 4 `@Before` / `@After` → JUnit 5 `@BeforeEach` / `@AfterEach`. +- `@BeforeClass` static method → `@BeforeAll` + `@JvmStatic` inside `companion object`. +- `@Test(expected = ...)` → `assertFailsWith { }` with reified generics. +- Static assertions become kotlin.test top-level function imports. +- Test method names use backtick syntax for readability. +- `users.size() > 0` becomes idiomatic `users.isNotEmpty()`. +- The `db` field uses `lateinit var` since it is initialized in `@BeforeAll`. diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md b/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md new file mode 100644 index 000000000000..337f7e395736 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md @@ -0,0 +1,253 @@ +# Mockito to MockK Conversion Guide + +## When This Applies + +Detected when imports match `org.mockito.*`. + +While Mockito works in Kotlin, MockK is the idiomatic Kotlin mocking library. Convert +to MockK when the project already uses MockK or is doing a full Kotlin migration. If +the project wants to keep Mockito, convert only the Java syntax to Kotlin syntax using +the `mockito-kotlin` helper library. + +## Key Rules + +### 1. MockK conversion table + +| Mockito | MockK | +|---|---| +| `Mockito.mock(Foo.class)` | `mockk()` | +| `@Mock Foo foo` | `@MockK lateinit var foo: Foo` (with `@ExtendWith(MockKExtension::class)`) | +| `when(foo.bar()).thenReturn(x)` | `every { foo.bar() } returns x` | +| `when(foo.bar()).thenThrow(e)` | `every { foo.bar() } throws e` | +| `when(foo.bar()).thenAnswer { }` | `every { foo.bar() } answers { }` | +| `doNothing().when(foo).bar()` | `justRun { foo.bar() }` | +| `verify(foo).bar()` | `verify { foo.bar() }` | +| `verify(foo, times(2)).bar()` | `verify(exactly = 2) { foo.bar() }` | +| `verify(foo, never()).bar()` | `verify(exactly = 0) { foo.bar() }` | +| `ArgumentCaptor` | `slot()` and `capture(slot)` | +| `any()` | `any()` | +| `eq(x)` | `eq(x)` (often not needed — MockK matches exact values by default) | +| `Mockito.spy(obj)` | `spyk(obj)` | +| `@InjectMocks` | No direct equivalent — use constructor injection | +| `verifyNoMoreInteractions(foo)` | `confirmVerified(foo)` | + +### 2. Coroutine support in MockK + +For suspending functions, use `coEvery` and `coVerify` instead of `every` and `verify`: +```kotlin +coEvery { foo.suspendBar() } returns x +coVerify { foo.suspendBar() } +``` + +### 3. Keeping Mockito (syntax-only conversion) + +If keeping Mockito, use the `mockito-kotlin` library (`org.mockito.kotlin`) for +Kotlin-friendly wrappers: +- `mock()` instead of `Mockito.mock(Foo::class.java)` — uses reified generics. +- `whenever(foo.bar())` instead of `` Mockito.`when`(foo.bar()) `` — avoids backtick- + escaping `when` (it is a Kotlin keyword). +- `argumentCaptor()` — type-safe captor via reified generics. +- `any()` — properly handles Kotlin's non-null types. + +### 4. Relaxed mocks + +MockK supports relaxed mocks that return default values without explicit stubbing: +`mockk(relaxed = true)`. This has no direct Mockito equivalent (Mockito's +`RETURNS_DEFAULTS` is the closest). + +--- + +## Example 1: Converting to MockK + +### Java Input + +```java +package com.acme.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.anyLong; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Tests for OrderService using Mockito mocks. + */ +public class OrderServiceTest { + + private UserRepository userRepository; + private OrderRepository orderRepository; + private OrderService orderService; + + @Before + public void setUp() { + userRepository = mock(UserRepository.class); + orderRepository = mock(OrderRepository.class); + orderService = new OrderService(userRepository, orderRepository); + } + + @Test + public void testCreateOrderForUser() { + User user = new User(1L, "Alice"); + when(userRepository.findById(1L)).thenReturn(user); + + orderService.createOrder(1L, "ITEM-100"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Order.class); + verify(orderRepository).save(captor.capture()); + assertEquals("ITEM-100", captor.getValue().getItemCode()); + assertEquals(1L, captor.getValue().getUserId()); + } + + @Test + public void testGetOrderCount() { + when(orderRepository.countByUserId(anyLong())).thenReturn(5); + + int count = orderService.getOrderCount(1L); + + assertEquals(5, count); + verify(orderRepository).countByUserId(1L); + } +} +``` + +### Kotlin Output (MockK) + +```kotlin +package com.acme.service + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +/** + * Tests for OrderService using MockK mocks. + */ +class OrderServiceTest { + + private val userRepository = mockk() + private val orderRepository = mockk() + private val orderService = OrderService(userRepository, orderRepository) + + @Test + fun `should create order for user`() { + val user = User(1L, "Alice") + every { userRepository.findById(1L) } returns user + every { orderRepository.save(any()) } returns Unit + + orderService.createOrder(1L, "ITEM-100") + + val orderSlot = slot() + verify { orderRepository.save(capture(orderSlot)) } + assertEquals("ITEM-100", orderSlot.captured.itemCode) + assertEquals(1L, orderSlot.captured.userId) + } + + @Test + fun `should return order count`() { + every { orderRepository.countByUserId(any()) } returns 5 + + val count = orderService.getOrderCount(1L) + + assertEquals(5, count) + verify { orderRepository.countByUserId(1L) } + } +} +``` + +**Key points:** +- `mock(Foo.class)` → `mockk()` using reified generics. +- `@Before` setUp is eliminated — mocks are initialized inline with property + declarations. This works because MockK mocks do not require a runner. +- `when(...).thenReturn(...)` → `every { ... } returns ...`. +- `ArgumentCaptor` → `slot()` with `capture(slot)`, accessed via `slot.captured`. +- `anyLong()` → `any()` (MockK's `any()` handles all types). +- `verify(foo).bar()` → `verify { foo.bar() }`. + +--- + +## Example 2: Keeping Mockito (mockito-kotlin syntax) + +### Java Input + +```java +package com.acme.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for PricingService using Mockito. + */ +public class PricingServiceTest { + + private PriceRepository priceRepository; + private PricingService pricingService; + + @Before + public void setUp() { + priceRepository = mock(PriceRepository.class); + pricingService = new PricingService(priceRepository); + } + + @Test + public void testGetPrice() { + when(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99); + double price = pricingService.getPrice("ITEM-1"); + assertEquals(9.99, price, 0.001); + verify(priceRepository).findPriceByItemCode("ITEM-1"); + } +} +``` + +### Kotlin Output (mockito-kotlin) + +```kotlin +package com.acme.service + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals + +/** + * Tests for PricingService using Mockito. + */ +class PricingServiceTest { + + private val priceRepository = mock() + private val pricingService = PricingService(priceRepository) + + @Test + fun `should return price for item`() { + whenever(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99) + + val price = pricingService.getPrice("ITEM-1") + + assertEquals(9.99, price, 0.001) + verify(priceRepository).findPriceByItemCode("ITEM-1") + } +} +``` + +**Key points:** +- `mock(Foo.class)` → `mock()` from `org.mockito.kotlin` (reified generics). +- `when(...)` → `whenever(...)` to avoid backtick-escaping the `when` keyword. +- `verify` stays the same — `org.mockito.kotlin.verify` wraps Mockito's verify. +- The `setUp` method is eliminated — mocks are initialized inline. +- `assertEquals` with a delta parameter works the same way from kotlin.test. diff --git a/AGENTS.md b/AGENTS.md index 5fdd984bd590..26b359f771f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,13 @@ Java, Kotlin, XML, Jetpack Compose are the key technologies used for building th - `./app/src/test/` Unit tests (small, isolated tests without Android SDK) - `./app/src/androidTest/` Instrumented tests (require Android SDK) - `./app/src/main/res/values/` Translations. Only update `./app/src/main/res/values/strings.xml`. Do not modify any other translation files or folders. Ignore all `values-*` directories (e.g., `values-es`, `values-fr`). +- `./.claude/skills/` Reusable agent skills. Each subdirectory is one skill with a `SKILL.md` entry point plus `references/` and `assets/`. + +## Agent Skills + +Project-specific skills live in `./.claude/skills//`. Load a skill when the task matches its trigger. + +- **`android-java-to-kotlin`** (`./.claude/skills/android-java-to-kotlin/SKILL.md`) — Completes a Java-to-Kotlin conversion in this Android app. Use it when finishing a conversion, when the user mentions "java to kotlin", "j2k", "convert java", or "make it idiomatic", or when a freshly IDE-converted `.kt` file needs cleanup. The workflow is two-person: the developer first runs the Android Studio converter (`Code > Convert Java File to Kotlin File`), then the agent drives the idiomatic second pass — fail-fast control flow, function decomposition, `lifecycleScope`/coroutines instead of Java threads, modern Android APIs, and project conventions (SPDX headers, no magic numbers, `@JvmStatic`). The conversion must preserve behaviour, and the agent must write a behaviour-locking test before declaring it done. Builds on the JetBrains java-to-kotlin methodology. ## General Guidance From e93750abc07a42f08019d6bbae864387dcd61706 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 13:58:46 +0200 Subject: [PATCH 02/11] wip Signed-off-by: alperozturk96 --- .claude/skills/android-java-to-kotlin/SKILL.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md index bfe109cdbd4a..45e4b50eee17 100644 --- a/.claude/skills/android-java-to-kotlin/SKILL.md +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -79,11 +79,8 @@ Scan imports and load ONLY the matching guides. |---|---| | `dagger.*`, `javax.inject.*` | [DAGGER-HILT.md](references/frameworks/DAGGER-HILT.md) | | `retrofit2.*`, `okhttp3.*` | [RETROFIT.md](references/frameworks/RETROFIT.md) | -| `io.reactivex.*`, `rx.*` | [RXJAVA.md](references/frameworks/RXJAVA.md) | | `org.junit.*` (test files) | [JUNIT.md](references/frameworks/JUNIT.md) | | `org.mockito.*` (test files) | [MOCKITO.md](references/frameworks/MOCKITO.md) | -| `com.fasterxml.jackson.*` | [JACKSON.md](references/frameworks/JACKSON.md) | -| `org.springframework.*` / `lombok.*` / `*.persistence.*` / `io.micronaut.*` / `io.quarkus.*` / `com.google.inject.*` | see `references/frameworks/` (rare in Android app code) | ## Step 2: Idiomatic Pass From fa29933a5f0f58f0638bcd88a357334557a516d2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:08:03 +0200 Subject: [PATCH 03/11] wip Signed-off-by: alperozturk96 --- .../skills/android-java-to-kotlin/references/CONCURRENCY.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md index fd172f986144..502b98e8455f 100644 --- a/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md +++ b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md @@ -105,12 +105,6 @@ private suspend fun loadAndPartitionShares(): Pair, List> } ``` -## RxJava Present? - -If the file uses `io.reactivex.*`/`rx.*`, load -[frameworks/RXJAVA.md](frameworks/RXJAVA.md) for the reactive-type → Flow/coroutine -mapping instead of hand-rolling. - ## Pitfalls - Never launch on `GlobalScope` — it outlives the screen and leaks. From 81c05d306249776ca5e85c16f739120f339a1fda Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:43:58 +0200 Subject: [PATCH 04/11] wip Signed-off-by: alperozturk96 --- .claude/skills/android-java-to-kotlin/SKILL.md | 2 ++ .../android-java-to-kotlin/assets/android-checklist.md | 7 +++++++ .claude/skills/android-java-to-kotlin/assets/checklist.md | 7 +++++++ .../android-java-to-kotlin/references/ANDROID-IDIOMS.md | 7 +++++++ .../android-java-to-kotlin/references/CONCURRENCY.md | 7 +++++++ .../references/CONVERSION-METHODOLOGY.md | 7 +++++++ .../skills/android-java-to-kotlin/references/FAIL-FAST.md | 7 +++++++ .../android-java-to-kotlin/references/KNOWN-ISSUES.md | 7 +++++++ 8 files changed, 51 insertions(+) diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md index 45e4b50eee17..bd0744459ba0 100644 --- a/.claude/skills/android-java-to-kotlin/SKILL.md +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -9,6 +9,8 @@ description: > second pass: idiomatic cleanup, fail-fast control flow, coroutines/lifecycleScope, modern Android APIs, function decomposition, and a behaviour-locking test. license: AGPL-3.0-or-later +SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later metadata: author: Nextcloud Android version: "1.0.0" diff --git a/.claude/skills/android-java-to-kotlin/assets/android-checklist.md b/.claude/skills/android-java-to-kotlin/assets/android-checklist.md index 848ba170526f..d5dd7701fc69 100644 --- a/.claude/skills/android-java-to-kotlin/assets/android-checklist.md +++ b/.claude/skills/android-java-to-kotlin/assets/android-checklist.md @@ -1,3 +1,10 @@ + + # Android Post-Conversion Checklist Walk this after Step 2–3, before declaring done. Complements the generic diff --git a/.claude/skills/android-java-to-kotlin/assets/checklist.md b/.claude/skills/android-java-to-kotlin/assets/checklist.md index 6b629b3262b6..f268a00560de 100644 --- a/.claude/skills/android-java-to-kotlin/assets/checklist.md +++ b/.claude/skills/android-java-to-kotlin/assets/checklist.md @@ -1,3 +1,10 @@ + + # Post-Conversion Verification Checklist Use this checklist after converting each Java file to Kotlin. diff --git a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md index 40d87b462f7e..f26561726ab7 100644 --- a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md +++ b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md @@ -1,3 +1,10 @@ + + # Android + Kotlin Idioms Transformations applied during the idiomatic pass. Every one is behaviour-preserving. diff --git a/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md index 502b98e8455f..da62469a6a5e 100644 --- a/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md +++ b/.claude/skills/android-java-to-kotlin/references/CONCURRENCY.md @@ -1,3 +1,10 @@ + + # Concurrency: Java Threads → Coroutines & `lifecycleScope` Replace `new Thread`, `AsyncTask`, executors, and `runOnUiThread`/`Handler.post` diff --git a/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md b/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md index 908fb2390a7b..79edee6df4e5 100644 --- a/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md +++ b/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md @@ -1,3 +1,10 @@ + + # Conversion Methodology You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is diff --git a/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md b/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md index a23743ffc6ed..79a0ef910f22 100644 --- a/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md +++ b/.claude/skills/android-java-to-kotlin/references/FAIL-FAST.md @@ -1,3 +1,10 @@ + + # Fail Fast: Guard Clauses Over Nested `if`/`else` The IDE preserves Java's nested-`if` pyramids. Invert them into guard clauses that return diff --git a/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md b/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md index 9de639d65ec2..671031166189 100644 --- a/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md +++ b/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md @@ -1,3 +1,10 @@ + + # Known Issues and Common Pitfalls A reference of common issues encountered during Java-to-Kotlin conversion, with solutions. From eb43c927cd8f35409597e08b3f7c9c14f6b5e502 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:44:09 +0200 Subject: [PATCH 05/11] wip Signed-off-by: alperozturk96 --- .../references/frameworks/DAGGER-HILT.md | 7 +++++++ .../android-java-to-kotlin/references/frameworks/JUNIT.md | 7 +++++++ .../references/frameworks/MOCKITO.md | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md index fe3636b92987..f373e2e5245f 100644 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md @@ -1,3 +1,10 @@ + + # Dagger / Hilt Conversion Guide ## When This Applies diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md index fb4a286e419b..c15ceac84af6 100644 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md @@ -1,3 +1,10 @@ + + # JUnit / TestNG Conversion Guide ## When This Applies diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md b/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md index 337f7e395736..b58adea36279 100644 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md +++ b/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md @@ -1,3 +1,10 @@ + + # Mockito to MockK Conversion Guide ## When This Applies From 932e23c17fb9fcc7be0a9cdfb4962bdc168de96a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:54:01 +0200 Subject: [PATCH 06/11] reduce scope to files client Signed-off-by: alperozturk96 --- .../skills/android-java-to-kotlin/SKILL.md | 34 +- .../assets/android-checklist.md | 70 ---- .../assets/checklist.md | 67 ---- .../references/CONVERSION-METHODOLOGY.md | 359 ----------------- .../references/KNOWN-ISSUES.md | 365 ------------------ .../references/frameworks/DAGGER-HILT.md | 167 -------- .../references/frameworks/JUNIT.md | 200 ---------- .../references/frameworks/MOCKITO.md | 260 ------------- 8 files changed, 5 insertions(+), 1517 deletions(-) delete mode 100644 .claude/skills/android-java-to-kotlin/assets/android-checklist.md delete mode 100644 .claude/skills/android-java-to-kotlin/assets/checklist.md delete mode 100644 .claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md delete mode 100644 .claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md delete mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md delete mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md delete mode 100644 .claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md index bd0744459ba0..defcb70f48ec 100644 --- a/.claude/skills/android-java-to-kotlin/SKILL.md +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later metadata: author: Nextcloud Android version: "1.0.0" - based-on: JetBrains kotlin-tooling-java-to-kotlin (Apache-2.0) --- # Android Java to Kotlin Conversion (Second Pass) @@ -42,8 +41,7 @@ digraph android_j2k { The **developer** runs the mechanical IDE conversion (`Code > Convert Java File to Kotlin File`, or ⌥⇧⌘K). **You (Claude)** complete everything after that. If you are handed a -`.java` file instead, first apply the faithful 1:1 translation in -[CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md) to reach the same +`.java` file instead, first apply the faithful 1:1 translation to reach the same starting point, then continue. ## The Prime Directive: Behaviour Must Not Change @@ -53,10 +51,6 @@ readability, safety, and modern API usage — not adding features or fixing bugs spot a real bug, surface it to the developer; do not silently "fix" it inside a conversion. The behaviour-locking test in Step 3 exists to keep you honest. -The 5 invariants from [CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md) -still apply: no new side-effects, preserve annotations/targets, preserve package, -preserve documentation (as KDoc), output valid Kotlin. - ## Step 0: Establish Baseline Before editing anything: @@ -64,33 +58,18 @@ Before editing anything: 1. Read the whole `.kt` file (and, if you can, the original `.java` via `git show :.java` or the IDE's local history) to understand *what it does*. 2. Write down the **public/observable surface** you must keep intact: - - Public and `@VisibleForTesting` method signatures called from other classes (Java - callers especially — see `@JvmStatic`/`@JvmField` in - [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md)). - Lifecycle callbacks (`onCreate`, `onViewCreated`, `onSaveInstanceState`, …) and their ordering of side-effects. - Any Parcelable/Bundle keys, intent extras, and `newInstance(...)` factory shapes. 3. Note threading: which work runs off the main thread today (`Thread`, `AsyncTask`, `runOnUiThread`, executors) — this maps to coroutines in Step 2. -## Step 1: Detect Frameworks - -Scan imports and load ONLY the matching guides. - -| Import prefix | Guide | -|---|---| -| `dagger.*`, `javax.inject.*` | [DAGGER-HILT.md](references/frameworks/DAGGER-HILT.md) | -| `retrofit2.*`, `okhttp3.*` | [RETROFIT.md](references/frameworks/RETROFIT.md) | -| `org.junit.*` (test files) | [JUNIT.md](references/frameworks/JUNIT.md) | -| `org.mockito.*` (test files) | [MOCKITO.md](references/frameworks/MOCKITO.md) | - -## Step 2: Idiomatic Pass +## Step 1: Idiomatic Pass Apply, in this order, then re-check the invariants: 1. **Kill platform types.** Give every `!` platform type an explicit nullable/non-null - type based on the Java source and call sites. See "Platform Types" in - [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md). + type based on the Java source and call sites. 2. **Fail fast.** Replace nested `if`/`else` pyramids and null-checks with guard clauses and `require`/`requireNotNull`/`?: return`. See [FAIL-FAST.md](references/FAIL-FAST.md). 3. **Decompose.** Break each oversized lifecycle callback / `setupView`-style method into @@ -111,7 +90,7 @@ Apply, in this order, then re-check the invariants: Do NOT expand scope. Unrelated files stay untouched (AGENTS.md / AI policy). -## Step 3: Write a Behaviour-Locking Test (Mandatory) +## Step 2: Write a Behaviour-Locking Test (Mandatory) A conversion is not complete until a test proves behaviour is unchanged. See [TESTING.md](references/TESTING.md) for the decision tree. In short: @@ -125,7 +104,7 @@ A conversion is not complete until a test proves behaviour is unchanged. See Every test file gets the SPDX header and follows the project's test conventions. -## Step 4: Verify +## Step 3: Verify Run and report real output — never claim green without evidence: @@ -141,9 +120,6 @@ Run and report real output — never claim green without evidence: -Pandroid.testInstrumentationRunnerArguments.class= ``` -Then walk [assets/android-checklist.md](assets/android-checklist.md). If anything fails or -behaviour drifted, return to Step 2. - ## Worked Example [assets/worked-example.md](assets/worked-example.md) is a real before/after from diff --git a/.claude/skills/android-java-to-kotlin/assets/android-checklist.md b/.claude/skills/android-java-to-kotlin/assets/android-checklist.md deleted file mode 100644 index d5dd7701fc69..000000000000 --- a/.claude/skills/android-java-to-kotlin/assets/android-checklist.md +++ /dev/null @@ -1,70 +0,0 @@ - - -# Android Post-Conversion Checklist - -Walk this after Step 2–3, before declaring done. Complements the generic -[checklist.md](checklist.md) (compilation, annotations, imports, nullability, collections). - -## Behaviour Preserved (Prime Directive) -- [ ] Same public / `@VisibleForTesting` signatures as before (Java callers still compile) -- [ ] Same exception types AND messages for the same preconditions -- [ ] Same Bundle keys, intent extras, and `newInstance(...)` wiring -- [ ] Same branch outcomes and same order of side-effects (snackbars, logs, DB writes) -- [ ] No feature added, no bug silently fixed inside the conversion - -## Fail Fast -- [ ] Nested `if`/`else` pyramids replaced with guard clauses / early returns -- [ ] `require` / `requireNotNull` / `check` used for preconditions (matching Java throw type) -- [ ] Resource cleanup (`cursor.close()`, streams) still runs on every early-return path - (or converted to `use {}`) - -## Function Decomposition -- [ ] Oversized lifecycle callbacks split into small, intent-named private functions -- [ ] Duplicated blocks factored into parameterized helpers -- [ ] File ≤300 lines (or split raised with developer / justified suppression noted) - -## Concurrency -- [ ] `new Thread` / `AsyncTask` / `runOnUiThread` replaced with `lifecycleScope` + `suspend` -- [ ] Correct dispatcher (`IO` for disk/net/DB, `Main` for views) -- [ ] `binding == null` / lifecycle guards preserved inside `launch` -- [ ] No `GlobalScope`; `CancellationException` not swallowed -- [ ] Timing-sensitive callers checked (sync DB read → async is flagged if observable) - -## Modern Android + Kotlin Idioms -- [ ] Platform types (`!`) all given explicit nullability -- [ ] Scope functions (`run`/`apply`/`let`) replace repeated `binding.`/`viewThemeUtils.` chains -- [ ] `switch` → `when` / `filter` + `partition` -- [ ] `TextUtils.isEmpty` → `isNullOrEmpty`; verbose utils → extension functions / KTX -- [ ] Getter/setter method calls → Kotlin property access - -## Project Conventions -- [ ] SPDX header replaced with current template + correct year -- [ ] Magic numbers → `const val` in `companion object` -- [ ] No hardcoded strings/colors/dimens (resources only; strings.xml only) -- [ ] `@JvmStatic` / `@JvmField` / `@JvmOverloads` / `@Throws` where Java calls in -- [ ] ≤120 cols, one type per file, exactly one trailing newline -- [ ] No decorative divider comments - -## Tests (Mandatory) -- [ ] Behaviour-locking test written (unit for pure logic, instrumented for components) -- [ ] Existing tests for this class still pass -- [ ] Test file has SPDX header and follows project test conventions -- [ ] Test asserts the observable contract, not implementation details - -## Verification Run -- [ ] `spotlessKotlinCheck` clean on changed files -- [ ] `detektGplayDebug` clean on changed files -- [ ] `lintGplayDebug` clean on changed files -- [ ] `spotbugsGplayDebug` clean on changed files -- [ ] Unit tests green (`jacocoTestGplayDebugUnitTest`) -- [ ] Actual command output reported (no unverified "it's green") - -## Git History -- [ ] `git mv` rename committed separately from content change -- [ ] Conventional Commit message + `Assisted-by:` trailer -- [ ] No `Signed-off-by` added by the agent; no autonomous PR/issue diff --git a/.claude/skills/android-java-to-kotlin/assets/checklist.md b/.claude/skills/android-java-to-kotlin/assets/checklist.md deleted file mode 100644 index f268a00560de..000000000000 --- a/.claude/skills/android-java-to-kotlin/assets/checklist.md +++ /dev/null @@ -1,67 +0,0 @@ - - -# Post-Conversion Verification Checklist - -Use this checklist after converting each Java file to Kotlin. - -## Compilation & Tests -- [ ] The `.kt` file compiles without errors -- [ ] All existing tests still pass -- [ ] No new compiler warnings introduced - -## Semantic Correctness -- [ ] No new side-effects or behavioural changes -- [ ] All public API signatures preserved (method names, parameter types, return types) -- [ ] Exception behaviour unchanged (same exceptions thrown in same conditions) - -## Annotations -- [ ] All annotations preserved from the original Java code -- [ ] Annotation site targets correct (`@field:`, `@get:`, `@set:`, `@param:`) -- [ ] No annotations accidentally dropped during conversion - -## Imports & Package -- [ ] Package declaration matches original -- [ ] All imports carried forward (except Java types that shadow Kotlin builtins) -- [ ] No new imports added unnecessarily - -## Documentation -- [ ] All Javadoc converted to KDoc format -- [ ] `{@code ...}` → backtick code in KDoc -- [ ] `{@link ...}` → `[...]` KDoc links -- [ ] `

` paragraph tags → blank lines -- [ ] `@param`, `@return`, `@throws` tags preserved -- [ ] Class-level and method-level documentation preserved - -## Nullability & Mutability -- [ ] Non-null types used only where provably non-null -- [ ] Nullable types (`?`) used for all Java types that could be null -- [ ] `val` used for all immutable variables/properties -- [ ] `var` used only for mutable variables/properties - -## Collections -- [ ] `MutableList`/`MutableSet`/`MutableMap` for Java's mutable collections -- [ ] `List`/`Set`/`Map` only where Java used immutable wrappers - -## Kotlin Idioms -- [ ] Getters/setters replaced with Kotlin properties where appropriate -- [ ] String concatenation replaced with string templates where clearer -- [ ] Elvis operator used where appropriate -- [ ] `when` expression used instead of `switch` -- [ ] Smart casts used after `is` checks (no explicit casts) - -## Framework-Specific (check applicable items) -- [ ] **Spring**: Classes that need proxying are `open`; `@Bean` methods are `open` -- [ ] **Lombok**: All Lombok annotations removed; replaced with Kotlin equivalents -- [ ] **Hibernate/JPA**: Entities are `open` (not data classes); no-arg constructor provided -- [ ] **Jackson**: `@field:` and `@get:` annotation site targets correct -- [ ] **RxJava**: Reactive types correctly mapped to Coroutines/Flow -- [ ] **Mockito**: `when` keyword escaped or replaced with MockK equivalent - -## Git History -- [ ] File renamed via `git mv` (not delete + create) -- [ ] Rename commit separate from content change commit diff --git a/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md b/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md deleted file mode 100644 index 79edee6df4e5..000000000000 --- a/.claude/skills/android-java-to-kotlin/references/CONVERSION-METHODOLOGY.md +++ /dev/null @@ -1,359 +0,0 @@ - - -# Conversion Methodology - -You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is -to convert provided Java code into **idiomatic Kotlin**, preserving behaviour while -improving readability, safety and maintainability. - -## The 4-Step Precognition Process - -Before emitting any code, run through the provided Java input and perform these 4 steps -of thinking. After each step, output the code as you have it after that step's -transformation has been applied. - -### Step 1: Faithful 1:1 Translation - -Convert the Java code 1 to 1 into Kotlin, prioritising faithfulness to the original -Java semantics, to replicate the Java code's functionality and logic exactly. - -**Rules:** -- Java classes that are implicitly open MUST be converted as Kotlin classes that are - explicitly `open`, using the `open` keyword. -- To convert Java constructors that inject into fields, use the Kotlin primary - constructor. Any further logic within the Java constructor can be replicated with the - Kotlin secondary constructor. - -### Step 2: Nullability & Mutability - -Check that mutability and nullability are correctly expressed in your Kotlin conversion. -Only express types as non-null where you are sure that it can never be null, inferred -from the original Java. Use `val` instead of `var` where you see variables that are -never modified. - -**Rules:** -- If you see a logical assertion that a value is not null (e.g., `Objects.requireNonNull`), - this shows that the author has considered that the value can never be null. Use a - non-null type in this case, and remove the logical assertion. -- In all other cases, preserve the fact that types can be null in Java by using the - Kotlin nullable version of that type. - -### Step 3: Collection Type Conversion - -Convert datatypes like collections from their Java variants to the Kotlin variants. - -**Rules:** -- For Java collections like `List` that are mutable by default, always use the Kotlin - `MutableList`, unless you see explicitly that the Java code uses an immutable wrapper - (e.g., `Collections.unmodifiableList()`) — in this case, use the Kotlin `List` (and - so on for other collections like `Set`, `Map` etc.) - -### Step 4: Idiomatic Transformations - -Introduce syntactic transformations to make the output truly idiomatic. - -**Rules:** -- Where getters and setters are defined as methods in Java, use the Kotlin syntax to - replace these methods with a more idiomatic version. -- Lambdas should be used where they can simplify code complexity while replicating the - exact behaviour of the previous code. - -## The 5 Invariants - -In each stage of your chain of thought, the following invariants must hold. - -**Invariant 1:** No new side-effects or behaviour. - -**Invariant 2:** Preserve all annotations and targets exactly. -- Annotations must target the backing field in Kotlin where they targeted the field in - Java. Use annotation site targets: `@field:`, `@get:`, `@set:`, `@param:`. - -**Invariant 3:** Preserve the package declaration and all imports. -- Carry forwards every single import, adding no new imports. Only remove imports where - they would shadow Kotlin names (e.g., `java.util.List` shadows Kotlin's `List`). - -**Invariant 4:** Preserve all Javadoc comments. -- In step 1, convert any Javadoc comments to KDoc comments. -- In all other steps, carry forwards the KDoc comments, preserving information where - structure changes. - -**Invariant 5:** Ensure the output result is in Kotlin. -- The emitted code must be syntactically valid Kotlin. - -**After each step**, go through each of these invariants, listing the ones that no -longer hold. If any exist, revert to the previous step and recalculate from there. - ---- - -## Example 1: Utility Class with Nullability - -### Java Input - -```java -package com.acme.util; - -import java.time.LocalDate; -import java.util.Objects; - -/** -* Utility for printing a greeting that includes the current date. -*/ -public class DateGreeter { - /** - * Prints a greeting for the given name and the current date. - * - * @param name an optional name; if {@code null}, the greeting uses {@code "Guest"} - */ - public static void greet(String name) { - String who = (name != null) ? name : "Guest"; - System.out.println("Hello, " + who + " - today is " + LocalDate.now()); - } -} -``` - -### Conversion Walkthrough - -**Step 1** — Faithful translation. The DateGreeter class is implicitly open, the -`greet` method is static → companion object. Javadoc converted to KDoc. - -```kotlin -package com.acme.util - -import java.time.LocalDate -import java.util.Objects - -/** -* Utility for printing a greeting that includes the current date. -*/ -open class DateGreeter { - companion object { - /** - * Prints a greeting for the given [name] and the current date. - * - * @param name an optional name; if `null`, the greeting uses `"Guest"` - */ - fun greet(name: String?) { - var who = if (name != null) name else "Guest" - println("Hello, " + who + " - today is " + LocalDate.now()) - } - } -} -``` - -Invariants check: All OK. - -**Step 2** — The `String?` is correct since the author checks for null. The `who` -variable is only read, so change `var` → `val`. - -```kotlin -// Same as above but with: -val who = if (name != null) name else "Guest" -``` - -Invariants check: All OK. - -**Step 3** — No collections in this code. No changes. - -Invariants check: All OK. - -**Step 4** — The `greet` function is not tied to any state of DateGreeter, so move it -to a top-level function. Use string templates and Elvis operator. - -```kotlin -package com.acme.util - -import java.time.LocalDate -import java.util.Objects - -/** -* Prints a greeting for the given [name] and the current date. -* -* @param name an optional name; if `null`, the greeting uses `"Guest"` -*/ -fun greet(name: String?) { - println("Hello, ${name ?: "Guest"} - today is ${LocalDate.now()}") -} -``` - -Invariants check: All OK. - ---- - -## Example 2: Domain Model with Annotations - -### Java Input - -```java -package com.acme.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.Nullable; -import java.util.Objects; - -/** -* Domain model for a user with a required identifier and an optional nickname. -*

-* The {@code id} is serialized as {@code "id"} and is required. -* The {@code nickname} may be absent. -*/ -public class User { - /** - * Stable, non-null identifier serialized as {@code "id"}. - */ - @JsonProperty("id") - private final String id; - - /** - * Optional nickname for display purposes. - */ - @Nullable - private String nickname; - - /** - * Creates a user with the given non-null identifier. - * - * @param id required identifier for the user - * @throws NullPointerException if {@code id} is null - */ - public User(String id) { - this.id = Objects.requireNonNull(id, "id"); - } - - /** - * Returns the identifier serialized as {@code "id"}. - * - * @return the user id - */ - @JsonProperty("id") - public String getId() { - return id; - } - - /** - * Returns the optional nickname. - * - * @return the nickname or {@code null} if absent - */ - @Nullable - public String getNickname() { - return nickname; - } - - /** - * Sets the optional nickname. - * - * @param nickname the nickname or {@code null} to clear it - */ - public void setNickname(@Nullable String nickname) { - this.nickname = nickname; - } -} -``` - -### Conversion Walkthrough - -**Step 1** — Faithful translation. Class is implicitly open → `open class`. -`@JsonProperty("id")` on the field → `@field:JsonProperty("id")`. -`@JsonProperty("id")` on the getter → `@get:JsonProperty("id")` when converted to -property later. Keep explicit getters/setters at this step for faithfulness. - -```kotlin -package com.acme.model - -import com.fasterxml.jackson.annotation.JsonProperty -import javax.annotation.Nullable -import java.util.Objects - -/** -* Domain model for a user with a required identifier and an optional nickname. -* -* The `id` is serialized as `"id"` and is required. -* The `nickname` may be absent. -*/ -open class User { - - /** - * Stable, non-null identifier serialized as `"id"`. - */ - @field:JsonProperty("id") - private val id: String - - /** - * Optional nickname for display purposes. - */ - @field:Nullable - private var nickname: String? = null - - /** - * Creates a user with the given non-null identifier. - * - * @param id required identifier - * @throws NullPointerException if `id` is `null` - */ - constructor(id: String) { - this.id = Objects.requireNonNull(id, "id") - } - - @get:JsonProperty("id") - fun getId(): String { return id } - - @Nullable - fun getNickname(): String? { return nickname } - - fun setNickname(@Nullable nickname: String?) { this.nickname = nickname } -} -``` - -Invariants check: All OK. - -**Step 2** — `id` is non-null by design (`Objects.requireNonNull` enforces it). -`nickname` is nullable (`@Nullable`). No val/var changes needed beyond what's already -done. Code unchanged. - -Invariants check: All OK. - -**Step 3** — No collections. No changes. - -Invariants check: All OK. - -**Step 4** — Idiomatic Kotlin: -1. Primary constructor with `id` as a `val` property. Apply both `@field:JsonProperty` - and `@get:JsonProperty` to match both Java annotation targets. -2. Convert `nickname` getter/setter → Kotlin property with `@field:Nullable` and - `@get:Nullable`. -3. Drop `Objects.requireNonNull` — Kotlin's type system enforces non-null. -4. Preserve all imports even if now unused (invariant 3). - -```kotlin -package com.acme.model - -import com.fasterxml.jackson.annotation.JsonProperty -import javax.annotation.Nullable -import java.util.Objects - -/** -* Domain model for a user with a required identifier and an optional nickname. -* -* The `id` is serialized as `"id"` and is required. -* The `nickname` may be absent. -* -* @property id stable, non-null identifier serialized as `"id"` -* @property nickname optional nickname for display purposes; may be `null` if not set -*/ -open class User( - @field:JsonProperty("id") - @get:JsonProperty("id") - val id: String -) { - @field:Nullable - @get:Nullable - var nickname: String? = null -} -``` - -Invariants check: All OK. diff --git a/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md b/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md deleted file mode 100644 index 671031166189..000000000000 --- a/.claude/skills/android-java-to-kotlin/references/KNOWN-ISSUES.md +++ /dev/null @@ -1,365 +0,0 @@ - - -# Known Issues and Common Pitfalls - -A reference of common issues encountered during Java-to-Kotlin conversion, with solutions. - -### Kotlin Keyword Conflicts - -Java identifiers that are reserved keywords in Kotlin will cause compilation errors after conversion. - -**Affected keywords:** `when`, `in`, `is`, `object`, `fun`, `val`, `var`, `typealias`, `as` - -**Solution:** Backtick-escape them in Kotlin: - -```java -// Java -public void when(String event) { ... } -public boolean in(List items) { ... } -``` - -```kotlin -// Kotlin — backtick-escaped -fun `when`(event: String) { ... } -fun `in`(items: List): Boolean { ... } -``` - -When the API is internal (not exposed to other modules), prefer renaming the identifier to a non-keyword alternative instead of using backticks. For example, rename `when` to `onEvent` or `in` to `contains`. - -### SAM Conversion Ambiguity - -When a Java method has overloads that each accept a different SAM (Single Abstract Method) interface, Kotlin's trailing lambda syntax becomes ambiguous. The compiler cannot determine which SAM interface the lambda should implement. - -```java -// Java — overloaded method accepting different SAM types -public class TaskExecutor { - void submit(Runnable task) { ... } - void submit(Callable task) { ... } -} -``` - -```kotlin -// Kotlin — WRONG: ambiguous, won't compile -executor.submit { doWork() } - -// Kotlin — CORRECT: explicit SAM constructor -executor.submit(Runnable { doWork() }) -executor.submit(Callable { computeResult() }) -``` - -Use explicit SAM constructor calls whenever there are overloaded methods accepting different functional interfaces. - -### Platform Types - -Java types without nullability annotations (`@Nullable`, `@NotNull`, `@NonNull`) become "platform types" (`T!`) in Kotlin. Platform types bypass Kotlin's null-safety system — they are neither nullable nor non-null, and null checks are deferred to runtime. - -```java -// Java — no nullability annotations -public String getName() { return name; } -public List getItems() { return items; } -``` - -```kotlin -// Kotlin — BAD: platform types left in converted code -val name = obj.name // inferred as String! — unsafe -val items = obj.items // inferred as List! — unsafe - -// Kotlin — GOOD: explicit nullability based on code analysis -val name: String = obj.name // if provably non-null -val name: String? = obj.name // if could be null -val items: List = obj.items // if neither list nor elements are null -``` - -Always add explicit type declarations to eliminate platform types. Analyze the Java source code, documentation, and call sites to determine the correct nullability. - -### @JvmStatic / @JvmField / @JvmOverloads - -When converted Kotlin code is still called from Java, use JVM interop annotations to maintain a clean Java API: - -**`@JvmStatic`** — Makes companion object functions accessible as static methods from Java: - -```kotlin -class Config { - companion object { - @JvmStatic - fun getInstance(): Config = ... - } -} -``` - -```java -// Java callers can use: Config.getInstance() -// Without @JvmStatic they would need: Config.Companion.getInstance() -``` - -**`@JvmField`** — Exposes a property as a direct field rather than through getter/setter: - -```kotlin -class Constants { - companion object { - @JvmField - val DEFAULT_TIMEOUT = 30_000L - } -} -``` - -```java -// Java callers can use: Constants.DEFAULT_TIMEOUT -// Without @JvmField they would need: Constants.Companion.getDEFAULT_TIMEOUT() -``` - -**`@JvmOverloads`** — Generates Java overloads for functions with default parameters: - -```kotlin -@JvmOverloads -fun connect(host: String, port: Int = 443, secure: Boolean = true) { ... } -``` - -```java -// Java sees three overloads: -// connect(String host) -// connect(String host, int port) -// connect(String host, int port, boolean secure) -``` - -### Checked Exceptions - -Kotlin does not have checked exceptions. When Kotlin code is called from Java, the Java compiler will not know about thrown exceptions unless annotated with `@Throws`: - -```kotlin -// Without @Throws, Java callers cannot catch IOException in a catch block -// (the Java compiler will say "exception is never thrown in the corresponding try block") - -@Throws(IOException::class) -fun readFile(path: String): String { - return File(path).readText() -} -``` - -Add `@Throws` to every Kotlin function that throws checked exceptions and is called from Java code. - -### Wildcard Generics - -Java wildcard types map to Kotlin's variance annotations: - -| Java | Kotlin | Description | -|------|--------|-------------| -| `? extends T` | `out T` | Covariance (producer) | -| `? super T` | `in T` | Contravariance (consumer) | -| Raw type `List` | `List` | Add explicit type parameter | - -```java -// Java -public void process(List numbers) { ... } -public void addAll(List target) { ... } -public void legacy(List items) { ... } // raw type -``` - -```kotlin -// Kotlin -fun process(numbers: List) { ... } -fun addAll(target: MutableList) { ... } -fun legacy(items: List) { ... } // explicit type parameter -``` - -For raw types, analyze the code to determine the most specific type parameter rather than defaulting to `Any?`. - -### Static Members - -Java's `static` keyword has no direct equivalent in Kotlin. Use the following mappings: - -**Static methods** — Use companion object functions, or top-level functions if they don't need class state: - -```java -// Java -public class StringUtils { - public static String capitalize(String s) { ... } -} -``` - -```kotlin -// Kotlin — top-level function (preferred when no class state needed) -fun capitalize(s: String): String { ... } - -// Kotlin — companion object (when logically tied to the class) -class StringUtils { - companion object { - fun capitalize(s: String): String { ... } - } -} -``` - -**Static constants** — Use `const val` for compile-time constants (primitives and String), `val` for object constants: - -```kotlin -class HttpStatus { - companion object { - const val OK = 200 // primitive — const val - const val NOT_FOUND_MESSAGE = "Not Found" // String — const val - val DEFAULT_HEADERS = mapOf("Accept" to "application/json") // object — val - } -} -``` - -**Static initializers** — Use companion object `init {}` block or top-level code: - -```kotlin -class Registry { - companion object { - private val handlers = mutableMapOf() - init { - handlers["default"] = DefaultHandler() - } - } -} -``` - -### Synchronized Blocks - -Java's `synchronized` constructs map to Kotlin as follows: - -**Synchronized blocks** — Use Kotlin's `synchronized()` function: - -```java -// Java -synchronized (lock) { - sharedState.update(); -} -``` - -```kotlin -// Kotlin -synchronized(lock) { - sharedState.update() -} -``` - -**Synchronized methods** — Use the `@Synchronized` annotation: - -```java -// Java -public synchronized void update() { ... } -``` - -```kotlin -// Kotlin -@Synchronized -fun update() { ... } -``` - -### Anonymous Inner Classes - -**Single Abstract Method (SAM) interfaces** — Convert to lambda syntax: - -```java -// Java -executor.submit(new Runnable() { - @Override - public void run() { - doWork(); - } -}); -``` - -```kotlin -// Kotlin -executor.submit(Runnable { doWork() }) -``` - -**Multiple methods or abstract classes** — Use `object` expression: - -```java -// Java -view.addListener(new ViewListener() { - @Override - public void onOpen() { ... } - @Override - public void onClose() { ... } -}); -``` - -```kotlin -// Kotlin -view.addListener(object : ViewListener { - override fun onOpen() { ... } - override fun onClose() { ... } -}) -``` - -### Array Handling - -Java arrays map to Kotlin types as follows: - -| Java | Kotlin | Notes | -|------|--------|-------| -| `String[]` | `Array` | Reference type arrays | -| `int[]` | `IntArray` | Primitive array (not `Array`) | -| `long[]` | `LongArray` | Primitive array | -| `double[]` | `DoubleArray` | Primitive array | -| `boolean[]` | `BooleanArray` | Primitive array | -| `Object[]` | `Array` | | -| `new int[10]` | `IntArray(10)` | Array creation | -| `new String[10]` | `arrayOfNulls(10)` | Nullable element array | -| `String... args` | `vararg args: String` | Varargs parameter | - -Using `Array` instead of `IntArray` causes boxing overhead — always use the specialized primitive array types. - -### Ternary Operator - -Kotlin has no ternary operator. Use `if`/`else` as an expression: - -```java -// Java -String label = (count > 0) ? "Items: " + count : "Empty"; -``` - -```kotlin -// Kotlin -val label = if (count > 0) "Items: $count" else "Empty" -``` - -### instanceof - -Java's `instanceof` maps to Kotlin's `is` keyword. Kotlin supports smart casting, so an explicit cast after an `is` check is unnecessary: - -```java -// Java -if (shape instanceof Circle) { - Circle circle = (Circle) shape; - double area = circle.getArea(); -} -``` - -```kotlin -// Kotlin — smart cast, no explicit cast needed -if (shape is Circle) { - val area = shape.area // shape is automatically cast to Circle -} -``` - -### try-with-resources - -Java's try-with-resources maps to Kotlin's `.use {}` extension function: - -```java -// Java -try (BufferedReader reader = new BufferedReader(new FileReader(path))) { - String line = reader.readLine(); - process(line); -} -``` - -```kotlin -// Kotlin -BufferedReader(FileReader(path)).use { reader -> - val line = reader.readLine() - process(line) -} -``` - -The `.use {}` function works on any `Closeable` or `AutoCloseable` instance and guarantees the resource is closed even if an exception is thrown. diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md deleted file mode 100644 index f373e2e5245f..000000000000 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/DAGGER-HILT.md +++ /dev/null @@ -1,167 +0,0 @@ - - -# Dagger / Hilt Conversion Guide - -## When This Applies - -This guide applies when the Java source contains imports matching `dagger.*` or -`dagger.hilt.*`. This covers Dagger 2, Hilt for Android, and Hilt Jetpack integrations. - -## Key Rules - -### 1. @Inject constructor syntax - -Kotlin places `@Inject` before the `constructor` keyword in the primary constructor: - -```kotlin -class Foo @Inject constructor(private val bar: Bar) -``` - -### 2. @Module classes with @Provides methods - -Keep `@Provides` methods `open`, or use `object` for modules that contain only -`@JvmStatic` provides methods (companion object pattern): - -```kotlin -@Module -@InstallIn(SingletonComponent::class) -object NetworkModule { - @Provides - @Singleton - fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build() -} -``` - -### 3. @Binds abstract methods - -`@Binds` methods work in abstract classes exactly as in Java. Convert the abstract -class directly — no special Kotlin considerations. - -### 4. Hilt Android annotations - -`@HiltAndroidApp`, `@AndroidEntryPoint`, `@HiltViewModel` — preserve these exactly -on Application, Activity, Fragment, and ViewModel classes. - -### 5. Scoping annotations - -`@Singleton`, `@ActivityScoped`, `@ViewModelScoped`, `@FragmentScoped` — preserve -exactly. No annotation site target is needed. - -### 6. @AssistedInject / @AssistedFactory - -`@AssistedInject` replaces `@Inject` on the constructor. `@Assisted` parameters -appear alongside regular injected parameters in the primary constructor: - -```kotlin -class PlayerViewModel @AssistedInject constructor( - @Assisted private val playerId: String, - private val repository: PlayerRepository -) : ViewModel() -``` - -### 7. @Component / @Subcomponent interfaces - -Convert directly to Kotlin interfaces. Dagger's annotation processing works -identically with Kotlin interfaces via kapt or KSP. - ---- - -## Examples - -### Example 1: Hilt ViewModel with @Inject Constructor and a @Module - -**Java:** - -```java -package com.acme.feature; - -import androidx.lifecycle.ViewModel; -import dagger.Module; -import dagger.Provides; -import dagger.hilt.InstallIn; -import dagger.hilt.android.lifecycle.HiltViewModel; -import dagger.hilt.components.SingletonComponent; -import javax.inject.Inject; -import javax.inject.Singleton; - -@HiltViewModel -public class UserProfileViewModel extends ViewModel { - - private final UserRepository userRepository; - private final AnalyticsTracker analyticsTracker; - - @Inject - public UserProfileViewModel(UserRepository userRepository, AnalyticsTracker analyticsTracker) { - this.userRepository = userRepository; - this.analyticsTracker = analyticsTracker; - } - - public LiveData getUser(String userId) { - analyticsTracker.trackProfileView(userId); - return userRepository.getUser(userId); - } -} - -@Module -@InstallIn(SingletonComponent.class) -public class AnalyticsModule { - - @Provides - @Singleton - public AnalyticsTracker provideAnalyticsTracker(Application app) { - return new AnalyticsTracker(app); - } -} -``` - -**Kotlin:** - -```kotlin -package com.acme.feature - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.components.SingletonComponent -import javax.inject.Inject -import javax.inject.Singleton - -@HiltViewModel -class UserProfileViewModel @Inject constructor( - private val userRepository: UserRepository, - private val analyticsTracker: AnalyticsTracker -) : ViewModel() { - - fun getUser(userId: String): LiveData { - analyticsTracker.trackProfileView(userId) - return userRepository.getUser(userId) - } -} - -@Module -@InstallIn(SingletonComponent::class) -object AnalyticsModule { - - @Provides - @Singleton - fun provideAnalyticsTracker(app: Application): AnalyticsTracker { - return AnalyticsTracker(app) - } -} -``` - -Key changes: -- `@Inject` moves before the `constructor` keyword in the primary constructor. -- Constructor parameters become `private val` in the primary constructor. -- The module class becomes an `object` since it contains only static-like provides methods. -- `SingletonComponent.class` becomes `SingletonComponent::class` (Kotlin class reference). -- Java getter method `getUser` becomes a regular function `getUser` (no `get` prefix - convention change needed here since it takes a parameter). diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md b/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md deleted file mode 100644 index c15ceac84af6..000000000000 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/JUNIT.md +++ /dev/null @@ -1,200 +0,0 @@ - - -# JUnit / TestNG Conversion Guide - -## When This Applies - -Detected when imports match `org.junit.*` or `org.testng.*`. - -## Key Rules - -### 1. JUnit 4 to Kotlin (with JUnit 5) - -| JUnit 4 | Kotlin (JUnit 5 / kotlin.test) | -|---|---| -| `@Test` | `@Test` (from `kotlin.test` or `org.junit.jupiter.api`) | -| `@Before` | `@BeforeEach` (JUnit 5) or `@BeforeTest` (kotlin.test) | -| `@After` | `@AfterEach` (JUnit 5) or `@AfterTest` (kotlin.test) | -| `@BeforeClass` | `@BeforeAll` in companion object with `@JvmStatic` | -| `@AfterClass` | `@AfterAll` in companion object with `@JvmStatic` | -| `@RunWith` | `@ExtendWith` (JUnit 5) | -| `@Ignore` | `@Disabled` (JUnit 5) | -| `@Rule` / `@ClassRule` | `@ExtendWith` or `@RegisterExtension` | -| `Assert.assertEquals(expected, actual)` | `assertEquals(expected, actual)` (kotlin.test) | -| `Assert.assertTrue(condition)` | `assertTrue(condition)` (kotlin.test) | -| `@Test(expected = X.class)` | `assertFailsWith { }` (kotlin.test) or `assertThrows { }` (JUnit 5) | - -### 2. JUnit 5 stays mostly the same - -JUnit 5 annotations (`@Test`, `@BeforeEach`, `@AfterEach`, etc.) remain unchanged. -Focus on Kotlin idioms in the test body: - -- `assertThrows { code }` — uses reified generics, no `.class` needed. -- Test classes and methods do not need to be `public` — Kotlin's default visibility - is public, which satisfies JUnit's requirements. -- Test methods do not need `open` unless using a framework that subclasses the test - (e.g., certain Spring test configurations). - -### 3. TestNG to Kotlin - -| TestNG | Kotlin (JUnit 5) | -|---|---| -| `@Test` | `@Test` | -| `@BeforeMethod` | `@BeforeEach` | -| `@AfterMethod` | `@AfterEach` | -| `@BeforeClass` | `@BeforeAll` with `@JvmStatic` in companion object | -| `@AfterClass` | `@AfterAll` with `@JvmStatic` in companion object | -| `@DataProvider` | `@ParameterizedTest` + `@MethodSource` | - -### 4. Assertion style - -Prefer `kotlin.test` assertions (`assertEquals`, `assertTrue`, `assertFailsWith`) -for portability across test frameworks. They delegate to the underlying framework -at runtime. - -### 5. Backtick method names - -Kotlin allows backtick-quoted method names for readable test names: -```kotlin -@Test -fun `should return empty list when no users exist`() { ... } -``` - ---- - -## Example: JUnit 4 Test Class to Kotlin with JUnit 5 - -### Java Input - -```java -package com.acme.service; - -import org.junit.Before; -import org.junit.After; -import org.junit.Test; -import org.junit.BeforeClass; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for the UserService class. - */ -public class UserServiceTest { - - private static DatabaseConnection db; - private UserService userService; - - @BeforeClass - public static void setupDatabase() { - db = DatabaseConnection.create("test"); - } - - @Before - public void setUp() { - userService = new UserService(db); - } - - @After - public void tearDown() { - db.clearTestData(); - } - - @Test - public void testFindById() { - User user = userService.findById(1L); - assertNotNull(user); - assertEquals("Alice", user.getName()); - } - - @Test - public void testFindAllReturnsNonEmptyList() { - List users = userService.findAll(); - assertNotNull(users); - assertTrue(users.size() > 0); - } - - @Test(expected = IllegalArgumentException.class) - public void testFindByIdWithNegativeIdThrows() { - userService.findById(-1L); - } -} -``` - -### Kotlin Output - -```kotlin -package com.acme.service - -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * Tests for the UserService class. - */ -class UserServiceTest { - - companion object { - private lateinit var db: DatabaseConnection - - @BeforeAll - @JvmStatic - fun setupDatabase() { - db = DatabaseConnection.create("test") - } - } - - private lateinit var userService: UserService - - @BeforeEach - fun setUp() { - userService = UserService(db) - } - - @AfterEach - fun tearDown() { - db.clearTestData() - } - - @Test - fun `should find user by id`() { - val user = userService.findById(1L) - assertNotNull(user) - assertEquals("Alice", user.name) - } - - @Test - fun `should return non-empty list from findAll`() { - val users = userService.findAll() - assertNotNull(users) - assertTrue(users.isNotEmpty()) - } - - @Test - fun `should throw IllegalArgumentException for negative id`() { - assertFailsWith { - userService.findById(-1L) - } - } -} -``` - -**Key points:** -- JUnit 4 `@Before` / `@After` → JUnit 5 `@BeforeEach` / `@AfterEach`. -- `@BeforeClass` static method → `@BeforeAll` + `@JvmStatic` inside `companion object`. -- `@Test(expected = ...)` → `assertFailsWith { }` with reified generics. -- Static assertions become kotlin.test top-level function imports. -- Test method names use backtick syntax for readability. -- `users.size() > 0` becomes idiomatic `users.isNotEmpty()`. -- The `db` field uses `lateinit var` since it is initialized in `@BeforeAll`. diff --git a/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md b/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md deleted file mode 100644 index b58adea36279..000000000000 --- a/.claude/skills/android-java-to-kotlin/references/frameworks/MOCKITO.md +++ /dev/null @@ -1,260 +0,0 @@ - - -# Mockito to MockK Conversion Guide - -## When This Applies - -Detected when imports match `org.mockito.*`. - -While Mockito works in Kotlin, MockK is the idiomatic Kotlin mocking library. Convert -to MockK when the project already uses MockK or is doing a full Kotlin migration. If -the project wants to keep Mockito, convert only the Java syntax to Kotlin syntax using -the `mockito-kotlin` helper library. - -## Key Rules - -### 1. MockK conversion table - -| Mockito | MockK | -|---|---| -| `Mockito.mock(Foo.class)` | `mockk()` | -| `@Mock Foo foo` | `@MockK lateinit var foo: Foo` (with `@ExtendWith(MockKExtension::class)`) | -| `when(foo.bar()).thenReturn(x)` | `every { foo.bar() } returns x` | -| `when(foo.bar()).thenThrow(e)` | `every { foo.bar() } throws e` | -| `when(foo.bar()).thenAnswer { }` | `every { foo.bar() } answers { }` | -| `doNothing().when(foo).bar()` | `justRun { foo.bar() }` | -| `verify(foo).bar()` | `verify { foo.bar() }` | -| `verify(foo, times(2)).bar()` | `verify(exactly = 2) { foo.bar() }` | -| `verify(foo, never()).bar()` | `verify(exactly = 0) { foo.bar() }` | -| `ArgumentCaptor` | `slot()` and `capture(slot)` | -| `any()` | `any()` | -| `eq(x)` | `eq(x)` (often not needed — MockK matches exact values by default) | -| `Mockito.spy(obj)` | `spyk(obj)` | -| `@InjectMocks` | No direct equivalent — use constructor injection | -| `verifyNoMoreInteractions(foo)` | `confirmVerified(foo)` | - -### 2. Coroutine support in MockK - -For suspending functions, use `coEvery` and `coVerify` instead of `every` and `verify`: -```kotlin -coEvery { foo.suspendBar() } returns x -coVerify { foo.suspendBar() } -``` - -### 3. Keeping Mockito (syntax-only conversion) - -If keeping Mockito, use the `mockito-kotlin` library (`org.mockito.kotlin`) for -Kotlin-friendly wrappers: -- `mock()` instead of `Mockito.mock(Foo::class.java)` — uses reified generics. -- `whenever(foo.bar())` instead of `` Mockito.`when`(foo.bar()) `` — avoids backtick- - escaping `when` (it is a Kotlin keyword). -- `argumentCaptor()` — type-safe captor via reified generics. -- `any()` — properly handles Kotlin's non-null types. - -### 4. Relaxed mocks - -MockK supports relaxed mocks that return default values without explicit stubbing: -`mockk(relaxed = true)`. This has no direct Mockito equivalent (Mockito's -`RETURNS_DEFAULTS` is the closest). - ---- - -## Example 1: Converting to MockK - -### Java Input - -```java -package com.acme.service; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.ArgumentMatchers.anyLong; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -/** - * Tests for OrderService using Mockito mocks. - */ -public class OrderServiceTest { - - private UserRepository userRepository; - private OrderRepository orderRepository; - private OrderService orderService; - - @Before - public void setUp() { - userRepository = mock(UserRepository.class); - orderRepository = mock(OrderRepository.class); - orderService = new OrderService(userRepository, orderRepository); - } - - @Test - public void testCreateOrderForUser() { - User user = new User(1L, "Alice"); - when(userRepository.findById(1L)).thenReturn(user); - - orderService.createOrder(1L, "ITEM-100"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Order.class); - verify(orderRepository).save(captor.capture()); - assertEquals("ITEM-100", captor.getValue().getItemCode()); - assertEquals(1L, captor.getValue().getUserId()); - } - - @Test - public void testGetOrderCount() { - when(orderRepository.countByUserId(anyLong())).thenReturn(5); - - int count = orderService.getOrderCount(1L); - - assertEquals(5, count); - verify(orderRepository).countByUserId(1L); - } -} -``` - -### Kotlin Output (MockK) - -```kotlin -package com.acme.service - -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals - -/** - * Tests for OrderService using MockK mocks. - */ -class OrderServiceTest { - - private val userRepository = mockk() - private val orderRepository = mockk() - private val orderService = OrderService(userRepository, orderRepository) - - @Test - fun `should create order for user`() { - val user = User(1L, "Alice") - every { userRepository.findById(1L) } returns user - every { orderRepository.save(any()) } returns Unit - - orderService.createOrder(1L, "ITEM-100") - - val orderSlot = slot() - verify { orderRepository.save(capture(orderSlot)) } - assertEquals("ITEM-100", orderSlot.captured.itemCode) - assertEquals(1L, orderSlot.captured.userId) - } - - @Test - fun `should return order count`() { - every { orderRepository.countByUserId(any()) } returns 5 - - val count = orderService.getOrderCount(1L) - - assertEquals(5, count) - verify { orderRepository.countByUserId(1L) } - } -} -``` - -**Key points:** -- `mock(Foo.class)` → `mockk()` using reified generics. -- `@Before` setUp is eliminated — mocks are initialized inline with property - declarations. This works because MockK mocks do not require a runner. -- `when(...).thenReturn(...)` → `every { ... } returns ...`. -- `ArgumentCaptor` → `slot()` with `capture(slot)`, accessed via `slot.captured`. -- `anyLong()` → `any()` (MockK's `any()` handles all types). -- `verify(foo).bar()` → `verify { foo.bar() }`. - ---- - -## Example 2: Keeping Mockito (mockito-kotlin syntax) - -### Java Input - -```java -package com.acme.service; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.Before; -import org.junit.Test; - -/** - * Tests for PricingService using Mockito. - */ -public class PricingServiceTest { - - private PriceRepository priceRepository; - private PricingService pricingService; - - @Before - public void setUp() { - priceRepository = mock(PriceRepository.class); - pricingService = new PricingService(priceRepository); - } - - @Test - public void testGetPrice() { - when(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99); - double price = pricingService.getPrice("ITEM-1"); - assertEquals(9.99, price, 0.001); - verify(priceRepository).findPriceByItemCode("ITEM-1"); - } -} -``` - -### Kotlin Output (mockito-kotlin) - -```kotlin -package com.acme.service - -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import kotlin.test.assertEquals - -/** - * Tests for PricingService using Mockito. - */ -class PricingServiceTest { - - private val priceRepository = mock() - private val pricingService = PricingService(priceRepository) - - @Test - fun `should return price for item`() { - whenever(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99) - - val price = pricingService.getPrice("ITEM-1") - - assertEquals(9.99, price, 0.001) - verify(priceRepository).findPriceByItemCode("ITEM-1") - } -} -``` - -**Key points:** -- `mock(Foo.class)` → `mock()` from `org.mockito.kotlin` (reified generics). -- `when(...)` → `whenever(...)` to avoid backtick-escaping the `when` keyword. -- `verify` stays the same — `org.mockito.kotlin.verify` wraps Mockito's verify. -- The `setUp` method is eliminated — mocks are initialized inline. -- `assertEquals` with a delta parameter works the same way from kotlin.test. From 9eccfb392a9f38d0a4c1693782034d6e1cb46a3c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:54:23 +0200 Subject: [PATCH 07/11] reduce scope to files client Signed-off-by: alperozturk96 --- .../android-java-to-kotlin/references/PROJECT-CONVENTIONS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md b/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md index 39f779079e82..5d2e15f89d00 100644 --- a/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md +++ b/.claude/skills/android-java-to-kotlin/references/PROJECT-CONVENTIONS.md @@ -52,8 +52,7 @@ asks for; default to the "Nextcloud GmbH and Nextcloud contributors" line. When the file still has Java callers, keep the Java-facing API clean: `@JvmStatic` for factory/companion functions, `@JvmField` for exposed constants, -`@JvmOverloads` for defaulted params, `@Throws` for checked exceptions. See -[KNOWN-ISSUES.md](KNOWN-ISSUES.md). +`@JvmOverloads` for defaulted params, `@Throws` for checked exceptions. ## Git & Commits (developer-driven) From 3cb58f324824530a7439f99e6e22277f267035f8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:55:17 +0200 Subject: [PATCH 08/11] reduce scope to files client Signed-off-by: alperozturk96 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 26b359f771f3..9614463afedf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Java, Kotlin, XML, Jetpack Compose are the key technologies used for building th Project-specific skills live in `./.claude/skills//`. Load a skill when the task matches its trigger. -- **`android-java-to-kotlin`** (`./.claude/skills/android-java-to-kotlin/SKILL.md`) — Completes a Java-to-Kotlin conversion in this Android app. Use it when finishing a conversion, when the user mentions "java to kotlin", "j2k", "convert java", or "make it idiomatic", or when a freshly IDE-converted `.kt` file needs cleanup. The workflow is two-person: the developer first runs the Android Studio converter (`Code > Convert Java File to Kotlin File`), then the agent drives the idiomatic second pass — fail-fast control flow, function decomposition, `lifecycleScope`/coroutines instead of Java threads, modern Android APIs, and project conventions (SPDX headers, no magic numbers, `@JvmStatic`). The conversion must preserve behaviour, and the agent must write a behaviour-locking test before declaring it done. Builds on the JetBrains java-to-kotlin methodology. +- **`android-java-to-kotlin`** (`./.claude/skills/android-java-to-kotlin/SKILL.md`) — Completes a Java-to-Kotlin conversion in this Android app. Use it when finishing a conversion, when the user mentions "java to kotlin", "j2k", "convert java", or "make it idiomatic", or when a freshly IDE-converted `.kt` file needs cleanup. The workflow is two-person: the developer first runs the Android Studio converter (`Code > Convert Java File to Kotlin File`), then the agent drives the idiomatic second pass — fail-fast control flow, function decomposition, `lifecycleScope`/coroutines instead of Java threads, modern Android APIs, and project conventions (SPDX headers, no magic numbers, `@JvmStatic`). The conversion must preserve behaviour, and the agent must write a behaviour-locking test before declaring it done. ## General Guidance From 0cb22d0d74c56706bd289967c0c7d75a7cccaf9f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 14:59:17 +0200 Subject: [PATCH 09/11] reduce scope to files client Signed-off-by: alperozturk96 --- .claude/skills/android-java-to-kotlin/SKILL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md index defcb70f48ec..173e80879e18 100644 --- a/.claude/skills/android-java-to-kotlin/SKILL.md +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -30,12 +30,11 @@ observable behaviour** — and then writes a test that proves behaviour is uncha digraph android_j2k { rankdir=TB; "Developer: IDE converts .java -> .kt" -> "Step 0: Establish baseline"; - "Step 0: Establish baseline" -> "Step 1: Detect frameworks"; "Step 1: Detect frameworks" -> "Step 2: Idiomatic pass"; "Step 2: Idiomatic pass" -> "Step 3: Write behaviour-locking test"; "Step 3: Write behaviour-locking test" -> "Step 4: Verify (build + checks + tests)"; "Step 4: Verify (build + checks + tests)" -> "Done" [label="green"]; - "Step 4: Verify (build + checks + tests)" -> "Step 2: Idiomatic pass" [label="fail / behaviour drift"]; + "Step 5: Verify (build + checks + tests)" -> "Step 2: Idiomatic pass" [label="fail / behaviour drift"]; } ``` From ba4fc10e289f8c2aaf4bb033baa9ffbccafd4d03 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 15:13:21 +0200 Subject: [PATCH 10/11] expand scope from previous conversion examples Signed-off-by: alperozturk96 --- .../skills/android-java-to-kotlin/SKILL.md | 29 +++--- .../references/ANDROID-IDIOMS.md | 2 + .../references/CONCURRENCY.md | 92 +++++++++++++++++++ .../references/DEPRECATED-APIS.md | 85 +++++++++++++++++ 4 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 .claude/skills/android-java-to-kotlin/references/DEPRECATED-APIS.md diff --git a/.claude/skills/android-java-to-kotlin/SKILL.md b/.claude/skills/android-java-to-kotlin/SKILL.md index 173e80879e18..1cec9a70feba 100644 --- a/.claude/skills/android-java-to-kotlin/SKILL.md +++ b/.claude/skills/android-java-to-kotlin/SKILL.md @@ -26,22 +26,17 @@ observable behaviour** — and then writes a test that proves behaviour is uncha ## The Two-Person Workflow -```dot -digraph android_j2k { - rankdir=TB; - "Developer: IDE converts .java -> .kt" -> "Step 0: Establish baseline"; - "Step 1: Detect frameworks" -> "Step 2: Idiomatic pass"; - "Step 2: Idiomatic pass" -> "Step 3: Write behaviour-locking test"; - "Step 3: Write behaviour-locking test" -> "Step 4: Verify (build + checks + tests)"; - "Step 4: Verify (build + checks + tests)" -> "Done" [label="green"]; - "Step 5: Verify (build + checks + tests)" -> "Step 2: Idiomatic pass" [label="fail / behaviour drift"]; -} -``` +This skill assumes a hand-off: + +1. **Developer** runs the mechanical IDE conversion (`Code > Convert Java File to Kotlin + File`, or ⌥⇧⌘K), producing a `.kt` file that compiles but is not idiomatic. +2. **You (Claude)** drive everything after that: + + Step 0 Baseline → Step 1 Idiomatic pass → Step 2 Behaviour-locking test → Step 3 + Verify. If Step 3 fails or behaviour drifts, loop back to Step 1. -The **developer** runs the mechanical IDE conversion (`Code > Convert Java File to Kotlin -File`, or ⌥⇧⌘K). **You (Claude)** complete everything after that. If you are handed a -`.java` file instead, first apply the faithful 1:1 translation to reach the same -starting point, then continue. +If you are handed a `.java` file instead, first apply a faithful 1:1 translation to reach +the same starting point, then continue. ## The Prime Directive: Behaviour Must Not Change @@ -79,7 +74,9 @@ Apply, in this order, then re-check the invariants: [CONCURRENCY.md](references/CONCURRENCY.md). 5. **Modern Android + Kotlin idioms.** Scope functions (`run`/`apply`/`let`), extension functions, `when`/`partition`/`filter` over `switch`, string templates, `isNullOrEmpty`, - view/KTX extensions. See [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md). + view/KTX extensions. See [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md). Retire + deprecated Android APIs (`onActivityResult`, options-menu overrides, + `java.util.Observable`) — see [DEPRECATED-APIS.md](references/DEPRECATED-APIS.md). 6. **Project conventions.** SPDX header, no magic numbers (`companion object` + `const val`), resources not hardcoded strings, ≤300 lines/file, ≤120 cols. See [PROJECT-CONVENTIONS.md](references/PROJECT-CONVENTIONS.md). diff --git a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md index f26561726ab7..1189e02f84c2 100644 --- a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md +++ b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md @@ -100,6 +100,8 @@ Import members directly and lean on AndroidX KTX instead of verbose Java utiliti | `BundleExtensionsKt.getParcelableArgument(b, k, T.class)` | `b.getParcelableArgument(k, T::class.java)` | | `for (int i = 0; i < vg.getChildCount(); i++)` | `for (i in 0.. { ... } +class GallerySearchTask( + private val fragment: GalleryFragment, + private val user: User, + private val storageManager: FileDataStorageManager, + private val endDate: Long, + private val limit: Int +) { + fun execute(): Job = fragment.lifecycleScope.launch(Dispatchers.IO) { + if (!isActive) return@launch + val context = fragment.context ?: return@launch + val result = performSearch(context) + withContext(Dispatchers.Main) { + fragment.searchCompleted(result.emptySearch, result.lastTimestamp) + } + } +} +``` + +The caller keeps the `Job` and cancels it with the view: + +```kotlin +private var photoSearchTask: Job? = null +// ... +photoSearchTask = GallerySearchTask(this, user, storageManager, endDate, limit).execute() + +override fun onDestroyView() { + photoSearchTask?.cancel() + photoSearchTask = null +} +``` + +Because `lifecycleScope` cancels on destroy, the old `WeakReference` leak-guard +disappears — a direct `fragment` reference plus `fragment.context ?: return@launch` is +enough. (PR #16908) + +## `AsyncTask` Behind a Callback API → `runCatching` + `fold` + +When a service method hid an `AsyncTask` that set mutable fields (a boolean success flag, +`errorMessage`, result fields) and dispatched them in `onPostExecute`, keep the public +callback signature but drive it from a coroutine. Return an **immutable** result from the +background function and branch with `fold`: + +```kotlin +private data class ActivitiesResult( + val activities: List, + val client: NextcloudClient, + val lastGiven: Long +) + +override fun getActivities(lastGiven: Long, callback: ActivitiesServiceCallback) { + scope.launch { + runCatching { withContext(Dispatchers.IO) { fetchActivities(lastGiven) } } + .fold( + onSuccess = { (activities, client, updated) -> + callback.onLoaded(activities, client, updated) + }, + onFailure = { callback.onError(it.message ?: "") } + ) + } +} +``` + +`runCatching` / `fold` replaces the boolean-success + `errorMessage`-field idiom; the +background function `fetchActivities` throws on failure instead of returning `false`, and +the immutable `ActivitiesResult` destructures straight into `onSuccess`. (PR #16654) + +> **Scope caveat:** that PR launched on an ad-hoc `CoroutineScope(Dispatchers.Main)` with +> no lifecycle owner — it never cancels and can leak. Prefer an injected `CoroutineScope` +> or expose a `suspend` function the presenter runs on `lifecycleScope` / `viewModelScope`. +> Flag ad-hoc scopes in review. + ## `new Thread { ... runOnUiThread(...) }` → `lifecycleScope.launch` + `withContext` ```kotlin @@ -115,6 +204,9 @@ private suspend fun loadAndPartitionShares(): Pair, List> ## Pitfalls - Never launch on `GlobalScope` — it outlives the screen and leaks. +- An ad-hoc `CoroutineScope(Dispatchers.Main)` created inside a service/presenter has the + same problem: no lifecycle owner, no cancellation, a leak. Use an injected scope or a + `suspend` function the caller runs on `lifecycleScope` / `viewModelScope`. - Keep `try/catch` semantics: coroutine cancellation throws `CancellationException`; rethrow it (don't swallow in a broad `catch (e: Exception)` that logs and continues) or catch `Exception` only around the real work, not around the whole `launch`. diff --git a/.claude/skills/android-java-to-kotlin/references/DEPRECATED-APIS.md b/.claude/skills/android-java-to-kotlin/references/DEPRECATED-APIS.md new file mode 100644 index 000000000000..944b78541ad2 --- /dev/null +++ b/.claude/skills/android-java-to-kotlin/references/DEPRECATED-APIS.md @@ -0,0 +1,85 @@ + + +# Retiring Deprecated Android APIs During Conversion + +A Java→Kotlin conversion is the right moment to replace deprecated Android APIs the IDE +converter leaves untouched. Each replacement below is behaviour-preserving. Examples are +real conversions (nextcloud/android PRs #16878, #16792). + +## `startActivityForResult` / `onActivityResult` → Activity Result API + +The request-code + `onActivityResult` protocol is deprecated. Register a launcher at +construction time and receive the result in its callback. + +```kotlin +// BEFORE +startActivityForResult(action, SELECT_LOCATION_REQUEST_CODE) +override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == SELECT_LOCATION_REQUEST_CODE && data != null) { handle(data) } +} + +// AFTER +private val folderPickerLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() +) { result -> + if (result.resultCode == Activity.RESULT_OK) { + handle(result.data) + } +} + +// launch it: +folderPickerLauncher.launch(intent) +``` + +Type-safe, no manual request-code bookkeeping, and it survives process death because +registration is declarative. Register during initialization (a field initializer or +`onCreate`/`onViewCreated`) — never inside a click handler, or the registration is lost. + +## `onCreateOptionsMenu` / `onOptionsItemSelected` → `MenuProvider` + +`setHasOptionsMenu(true)` plus the two menu overrides are deprecated on `Fragment`. Add a +`MenuProvider` bound to the view lifecycle instead. + +```kotlin +// AFTER +val menuHost: MenuHost = requireActivity() +menuHost.addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, inflater: MenuInflater) = + inflater.inflate(R.menu.gallery_menu, menu) + + override fun onMenuItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.action_select_all -> { selectAll(); true } + else -> false + } +}, viewLifecycleOwner, Lifecycle.State.RESUMED) +``` + +Passing `viewLifecycleOwner` + `Lifecycle.State.RESUMED` auto-adds and removes the menu as +the view's lifecycle changes — no leak, no manual `setHasOptionsMenu`. (PR #16878) + +## `java.util.Observable` — Keep or Migrate? + +`java.util.Observable` / `Observer` are deprecated (Java 9+). But a conversion is +behaviour-locked, so **do not** silently swap the notification mechanism — existing Java +observers rely on `setChanged()` / `notifyObservers()`. PR #16792 deliberately KEPT it: + +```kotlin +class UploadsStorageManager(...) : Observable() { + fun notifyObserversNow() { + Handler(Looper.getMainLooper()).post { + setChanged() + notifyObservers() + } + } +} +``` + +Migrating to `StateFlow` / `SharedFlow` changes the observation contract and every call +site — that is a separate, opt-in refactor, not part of a 1:1 conversion. Note the +deprecation, propose the flow migration as a follow-up, and keep the current mechanism +unless the developer scopes the larger change. From 5552989bab893e7f5e5cd44b438ed69956a415b8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 15:14:25 +0200 Subject: [PATCH 11/11] expand scope from previous conversion examples Signed-off-by: alperozturk96 --- .../skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md index 1189e02f84c2..b669874f1873 100644 --- a/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md +++ b/.claude/skills/android-java-to-kotlin/references/ANDROID-IDIOMS.md @@ -162,7 +162,7 @@ If a legacy god-class genuinely cannot be split within the conversion's scope, a file/class-level `@Suppress("TooManyFunctions", "LargeClass", ...)` is acceptable — but prefer real decomposition and tell the developer what you suppressed and why. -## 8. Optional: `// region` Organization +## 8. `// region` Organization For large classes, grouping members under `// region ` / `// endregion` (lifecycle, private methods, overrides, companion) aids IDE folding. This is IDE structure, not a