Skip to content

feat(kotlin): Upgrade project to Kotlin 2.2 and KSP#484

Merged
kingargyle merged 1 commit intodevelopfrom
dac_kotlin_2221
Nov 17, 2025
Merged

feat(kotlin): Upgrade project to Kotlin 2.2 and KSP#484
kingargyle merged 1 commit intodevelopfrom
dac_kotlin_2221

Conversation

@kingargyle
Copy link
Contributor

@kingargyle kingargyle commented Nov 17, 2025

Upgrades the project to use Kotlin 2.2, which includes migrating from kapt to KSP (Kotlin Symbol Processing) for annotation processors like Toothpick and Glide.

This migration addresses several underlying build and dependency injection issues:

  • Kotlin & Plugin Versions:

    • Upgrades the core Kotlin language version to 2.2.21.
    • Updates the KSP plugin to version 2.2.21-2.0.4 to match the Kotlin version.
    • Upgrades related dependencies like Coroutines, MockK, and the Android Gradle Plugin to ensure compatibility.
  • Build Configuration:

    • Cleans up legacy build files and duplicate class definitions that were causing KSP to fail during code generation. Specifically, MovieMediaContainer.kt and EpisodeMediaContainer.kt were moved from the java to the kotlin source directory.
  • Code Modernization & Bug Fixes:

    • Replaces deprecated toLowerCase() calls with the modern lowercase() equivalent.
    • Updates Leanback presenter classes (CardPresenter, MoviePresenter, etc.) to conform to the new, nullable onBindViewHolder method signature required by the updated library versions.
    • Fixes a critical dependency injection issue in unit tests by removing the premature Toothpick.inject call from the BaseInjector's init block. This ensures that test scopes and mock bindings are correctly applied before injection occurs.
    • Updates all relevant unit tests to explicitly inject manually created container objects using the test scope, following the established pattern in InjectingTest.

This comprehensive upgrade modernizes the build system, improves build reliability by resolving KSP code generation failures, and fixes several bugs and test failures that arose from the version changes.

Closes #480

Summary by CodeRabbit

  • Chores

    • Updated build tools and dependency versions for improved compatibility and performance.
    • Modernized codebase by replacing deprecated APIs with current standard library methods.
    • Removed unused build configuration entries.
  • Tests

    • Enhanced test infrastructure with improved dependency injection setup.

Upgrades the project to use Kotlin 2.2, which includes migrating from `kapt` to KSP (Kotlin Symbol Processing) for annotation processors like Toothpick and Glide.

This migration addresses several underlying build and dependency injection issues:

- **Kotlin & Plugin Versions:**
  - Upgrades the core Kotlin language version to `2.2.21`.
  - Updates the KSP plugin to version `2.2.21-2.0.4` to match the Kotlin version.
  - Upgrades related dependencies like Coroutines, MockK, and the Android Gradle Plugin to ensure compatibility.

- **Build Configuration:**
  - Cleans up legacy build files and duplicate class definitions that were causing KSP to fail during code generation. Specifically, `MovieMediaContainer.kt` and `EpisodeMediaContainer.kt` were moved from the `java` to the `kotlin` source directory.

- **Code Modernization & Bug Fixes:**
  - Replaces deprecated `toLowerCase()` calls with the modern `lowercase()` equivalent.
  - Updates Leanback presenter classes (`CardPresenter`, `MoviePresenter`, etc.) to conform to the new, nullable `onBindViewHolder` method signature required by the updated library versions.
  - Fixes a critical dependency injection issue in unit tests by removing the premature `Toothpick.inject` call from the `BaseInjector`'s `init` block. This ensures that test scopes and mock bindings are correctly applied before injection occurs.
  - Updates all relevant unit tests to explicitly inject manually created container objects using the test scope, following the established pattern in `InjectingTest`.

This comprehensive upgrade modernizes the build system, improves build reliability by resolving KSP code generation failures, and fixes several bugs and test failures that arose from the version changes.

Closes #480
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

Walkthrough

This PR upgrades the project to Kotlin 2.2.21, migrates build configuration to use version catalog aliases, converts BaseInjector from Java to Kotlin, restructures dependency injection for Resources across media container classes, updates presenter method signatures to accept nullable items, replaces deprecated toLowerCase() calls with lowercase(), and removes unused code and imports.

Changes

Cohort / File(s) Summary
Gradle & Build Configuration
build.gradle, gradle/libs.versions.toml, gradle/wrapper/gradle-wrapper.properties, serenity-common/build.gradle.kts
Updated KSP plugin declaration to use version catalog alias; upgraded Kotlin to 2.2.21, KSP to 2.2.21-2.0.4, Kotlin Coroutines to 1.10.2, AGP to 8.13.1, Mockk to 1.14.6; bumped Gradle wrapper from 8.13 to 8.14.3; removed unused Kotlin DSL imports.
Kotlin API Modernization
serenity-android-common/src/main/kotlin/us/nineworlds/serenity/common/android/mediacodec/MediaCodecInfoUtil.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/util/ImageInfographicUtils.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
Replaced deprecated toLowerCase() with lowercase() across three files.
Dependency Injection Restructuring
serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/AbstractMediaContainer.kt, serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/MenuMediaContainer.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt
Added Resources field injection to AbstractMediaContainer; removed Resources injection from MenuMediaContainer and EpisodeMediaContainer; added @Suppress("Injectable") annotation to EpisodeMediaContainer.
BaseInjector Migration
serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java (removed), serenity-app/src/main/kotlin/us/nineworlds/serenity/injection/BaseInjector.kt (added)
Migrated BaseInjector from Java to Kotlin; maintains constructor-based DI via Toothpick APPLICATION_SCOPE and getActivity() helper method for unwrapping ContextWrapper to Activity.
Presenter Method Signature Updates
serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/search/CardPresenter.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/CategoryVideoPresenter.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/EpisodeVideoPresenter.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/MoviePresenter.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/SeriesPresenter.kt, serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/VideoContentInfoPresenter.kt
Changed onBindViewHolder item parameter from non-nullable Any to nullable Any? across six presenter classes; SeriesPresenter also updated onUnbindViewHolder to non-null ViewHolder with no-op implementation.
Build Configuration & Test Updates
serenity-app/build.gradle.kts, serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt, serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt, serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt
Removed commented-out kaptTest dependency; added Resources mock bindings in test modules; introduced TestModule in CategoryMediaContainerTest with SerenityClient and Resources bindings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • BaseInjector migration — Verify Java-to-Kotlin conversion preserves all DI semantics and getActivity() logic.
  • Presenter nullable parameter changes — Confirm all six presenters handle null items safely; check downstream call sites for compatibility.
  • Dependency injection restructuring — Ensure Resources injection movement from MenuMediaContainer and EpisodeMediaContainer to AbstractMediaContainer does not break initialization order or cause cascading DI failures.
  • Test module updates — Validate mock bindings for Resources and SerenityClient are properly scoped and do not conflict with production DI.

Possibly related PRs

Poem

🐰 Kotlin hops from two-point-two-two,
toLowerCase bids its fond adieu!
BaseInjector leaps in Kotlin attire,
Resources flow through fields we desire,
Presenters now accept what might be nil—
Our builds evolve, and tests stay shrill! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main change: upgrading Kotlin to 2.2 and updating KSP plugin, which aligns with the primary objective.
Linked Issues check ✅ Passed All coding objectives from issue #480 are met: Kotlin stdlib upgraded to 2.2.21, KSP plugin updated to 2.2.21-2.0.4, related dependencies updated for compatibility, deprecated toLowerCase() replaced with lowercase(), and build/test infrastructure modernized.
Out of Scope Changes check ✅ Passed All changes are directly related to the Kotlin/KSP upgrade objective. Refactoring of presenters to nullable signatures, migration from kapt to KSP, file relocations, and test fixture updates are all necessary for the upgrade compatibility.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dac_kotlin_2221

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/EpisodeVideoPresenter.kt (1)

23-27: Add null-safety check before casting nullable item.

The nullable item parameter is cast to EpisodePosterInfo without a null check. This matches the same pattern seen in other presenters and should be addressed consistently.

Apply null-safety:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
     val imageWidth = viewHolder.view.context.resources.getDimensionPixelSize(R.dimen.episode_image_width)
     val imageHeight = viewHolder.view.context.resources.getDimensionPixelSize(R.dimen.episode_image_height)
 
-    val video = item as EpisodePosterInfo
+    val video = item as? EpisodePosterInfo ?: return
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/MoviePresenter.kt (1)

16-17: Add null-safety check before casting nullable item.

Same issue as in VideoContentInfoPresenter.kt: the nullable item parameter is cast to VideoContentInfo without a null check, risking a runtime exception if null is passed.

Apply the same null-safety pattern:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
-    val videoInfo = item as VideoContentInfo
+    val videoInfo = item as? VideoContentInfo ?: return
     val vh = viewHolder as MoviePresenterViewHolder
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/SeriesPresenter.kt (1)

20-22: Add null-safety check before casting nullable item.

Consistent with other presenters, the nullable item parameter is cast to TVShowSeriesInfo without a null check, creating a potential runtime exception.

Apply null-safety:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
     val vh = viewHolder as SeriesPresenterViewHolder
-    vh.bind(item as TVShowSeriesInfo)
+    val seriesInfo = item as? TVShowSeriesInfo ?: return
+    vh.bind(seriesInfo)
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/CategoryVideoPresenter.kt (1)

23-24: Add null-safety check before casting nullable item.

This presenter exhibits the same unsafe cast pattern as the others in this PR. The nullable item is cast to VideoCategory without validation.

Apply null-safety:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
-    val video = item as VideoCategory
+    val video = item as? VideoCategory ?: return
🧹 Nitpick comments (2)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt (1)

3-3: LGTM! Consider removing the extra space for consistency.

The Resources binding correctly supports the production DI changes. However, there's a minor formatting inconsistency.

Apply this diff to align with the formatting on line 125:

-      bind( Resources::class.java).toInstance(mockk())
+      bind(Resources::class.java).toInstance(mockk())

Also applies to: 126-126

serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt (1)

3-3: LGTM! Consider minor formatting consistency.

The test module correctly provides mock bindings for the DI framework to support the production changes.

Two minor consistency refinements:

  1. Remove the extra space on line 98 to match line 97:
-            bind( Resources::class.java).toInstance(mockk())
+            bind(Resources::class.java).toInstance(mockk())
  1. For consistency with AbstractMediaContainerTest and MovieMediaContainerTest, consider making TestModule an inner class (though not strictly necessary since it doesn't reference outer class members):
-    class TestModule : Module() {
+    inner class TestModule : Module() {

Also applies to: 15-15, 17-17, 53-53, 95-100

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d689ff0 and c052054.

📒 Files selected for processing (22)
  • build.gradle (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
  • gradle/wrapper/gradle-wrapper.properties (1 hunks)
  • serenity-android-common/src/main/kotlin/us/nineworlds/serenity/common/android/mediacodec/MediaCodecInfoUtil.kt (2 hunks)
  • serenity-app/build.gradle.kts (0 hunks)
  • serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/AbstractMediaContainer.kt (2 hunks)
  • serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/MenuMediaContainer.kt (0 hunks)
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java (0 hunks)
  • serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/search/CardPresenter.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt (2 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/injection/BaseInjector.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/CategoryVideoPresenter.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/EpisodeVideoPresenter.kt (2 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/MoviePresenter.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/SeriesPresenter.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/VideoContentInfoPresenter.kt (1 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/util/ImageInfographicUtils.kt (3 hunks)
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt (1 hunks)
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt (2 hunks)
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt (4 hunks)
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt (2 hunks)
  • serenity-common/build.gradle.kts (0 hunks)
💤 Files with no reviewable changes (4)
  • serenity-app/build.gradle.kts
  • serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/MenuMediaContainer.kt
  • serenity-common/build.gradle.kts
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java
🧰 Additional context used
🧬 Code graph analysis (4)
serenity-app/src/main/kotlin/us/nineworlds/serenity/injection/BaseInjector.kt (1)
serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java (1)
  • BaseInjector (32-48)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt (4)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt (1)
  • bind (95-100)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt (1)
  • bind (123-128)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractVideoContentInfoTest.kt (1)
  • bind (159-164)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainerTest.kt (1)
  • bind (156-161)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt (2)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt (1)
  • bind (62-67)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt (1)
  • bind (123-128)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/MovieMediaContainerTest.kt (4)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt (1)
  • bind (62-67)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/CategoryMediaContainerTest.kt (1)
  • bind (95-100)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractVideoContentInfoTest.kt (1)
  • bind (159-164)
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainerTest.kt (1)
  • bind (156-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
serenity-android-common/src/main/kotlin/us/nineworlds/serenity/common/android/mediacodec/MediaCodecInfoUtil.kt (2)

76-76: LGTM! Correct deprecation fix.

The replacement of toLowerCase() with lowercase() aligns with Kotlin 2.2 best practices and maintains the same case-conversion behavior for mime-type matching.


88-88: LGTM! Consistent deprecation fix.

The replacement of toLowerCase() with lowercase() is correct and consistent with the modernization applied throughout the codebase.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt (1)

175-175: LGTM! Kotlin API modernization applied correctly.

The replacement of toLowerCase() with lowercase() is the correct approach for Kotlin 2.x, and this change aligns with the PR's objective to modernize deprecated API usage.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/util/ImageInfographicUtils.kt (1)

94-94: LGTM! Correct modernization of deprecated API.

The replacement of toLowerCase() with lowercase() at these three locations correctly modernizes the code for Kotlin 2.2. The behavior remains identical.

Also applies to: 152-152, 184-184

serenity-app/src/test/kotlin/us/nineworlds/serenity/core/model/impl/AbstractMediaContainerTest.kt (1)

3-3: LGTM! Test DI properly supports the new Resources injection.

The import and mock binding for Resources correctly align with the production code changes in AbstractMediaContainer.kt that added an injected Resources field.

Also applies to: 65-65

serenity-app/src/main/java/us/nineworlds/serenity/core/model/impl/AbstractMediaContainer.kt (1)

3-3: LGTM! Resources injection follows established patterns.

The addition of the injected Resources field aligns with the DI restructuring described in the PR objectives and follows the same pattern as the existing factory field.

Also applies to: 22-23

serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt (1)

11-12: No issues found — code correctly inherits resources from parent class.

Verification confirms that the resources field is properly available and injected through the inheritance chain. AbstractMediaContainer declares @Inject lateinit var resources: Resources, which is inherited by MovieMediaContainer and then by EpisodeMediaContainer. The @Suppress("Injectable") annotation is appropriate because the class takes a non-injectable constructor parameter.

serenity-app/src/main/kotlin/us/nineworlds/serenity/injection/BaseInjector.kt (1)

15-24: LGTM: Clean Kotlin conversion of getActivity utility.

The method correctly converts the Java implementation to idiomatic Kotlin using the is operator for type checks and property access for baseContext. The nullable return type and loop logic are sound.

gradle/libs.versions.toml (2)

139-139: Version catalog plugin entry follows best practices.

The new KSP plugin entry enables centralized version management and allows subprojects to reference it via alias(libs.plugins.ksp) as shown in the root build.gradle changes. The syntax is correct and the version reference properly points to the updated kotlinKSPVersion.


17-20: Version updates verified as compatible across Kotlin/Coroutines/KSP/MockK ecosystem.

MockK 1.14.6 is compatible with Kotlin 2.2.21, and kotlinx.coroutines 1.10.2 is compatible with Kotlin 2.2.21. The KSP version correctly aligns with Kotlin 2.2.21, and the Android Plugin micro-bump is low-risk. All dependency version updates are safe.

build.gradle (2)

19-19: Plugin alias migration to version catalog is clean and idiomatic.

Using alias(libs.plugins.ksp) centralizes KSP version management and is the modern Gradle best practice. The apply false correctly makes the plugin available to subprojects without applying it to the root project itself.


19-19: Plugin alias migration to version catalog is clean and idiomatic.

Using alias(libs.plugins.ksp) centralizes KSP version management and is the modern Gradle best practice. The first part of the KSP version must match the version of Kotlin being used in your build, and KSP 2.2.21-2.0.4 correctly aligns with Kotlin 2.2.21. The apply false correctly makes the plugin available to subprojects without applying it to the root project itself.

gradle/wrapper/gradle-wrapper.properties (2)

4-4: Gradle wrapper update to 8.14.3 is compatible with Kotlin 2.2.21 and KSP 2.2.21-2.0.4.

The patch bump from Gradle 8.13 to 8.14.3 is safe and well-aligned with the Kotlin upgrade. Kotlin 2.0.20–2.0.21 and Kotlin 2.1.0–2.1.10 are fully compatible with Gradle up to 8.6, and Gradle versions 8.7–8.10 are also supported, ensuring Gradle 8.14.3 compatibility with Kotlin 2.2.21.


4-4: Gradle wrapper update to 8.14.3 is compatible and approved.

Kotlin 2.2.21 is fully supported on Gradle 8.14.3, and KSP 2.2.21-2.0.4 is an appropriate release for use with Kotlin 2.2.21 on Gradle 8.14.3. The patch bump is safe and recommended.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/EpisodeVideoPresenter.kt (1)

57-59: Good defensive null-safety check.

The null check for mainImageView before loading the image is good defensive coding practice and prevents potential crashes if the view is not yet initialized.

Comment on lines +57 to 58
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
val video = item as VideoContentInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Unsafe cast will throw exception if item is null.

The parameter item is now nullable (Any?), but line 58 performs an unsafe cast without null checking. If null is passed, this will cause a runtime exception.

Apply this diff to safely handle null items:

-    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
-        val video = item as VideoContentInfo
+    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
+        val video = item as? VideoContentInfo ?: return

The AI summary indicates similar signature changes were made to other presenters (CategoryVideoPresenter, EpisodeVideoPresenter, MoviePresenter, SeriesPresenter, VideoContentInfoPresenter). Please verify that those files also handle null items safely:


🏁 Script executed:

#!/bin/bash
# Description: Check for unsafe casts in other updated presenters

# Search for onBindViewHolder methods with nullable items followed by unsafe casts
rg -A 2 'onBindViewHolder.*item:\s*Any\?' serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/ | rg -A 1 'as\s+\w+' | rg -B 1 'as\s+(?!.*\?)'

Length of output: 221


🏁 Script executed:

#!/bin/bash
# Description: Find onBindViewHolder methods with nullable items and check for unsafe casts

# First, find all presenter files that have been modified
echo "=== Finding presenter files with onBindViewHolder methods ==="
fd -e "Presenter\.kt$" serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/

echo -e "\n=== Checking onBindViewHolder implementations for unsafe casts ==="
# Search for onBindViewHolder with nullable item parameter
rg -A 5 'override fun onBindViewHolder.*item:\s*Any\?' serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/ --type kotlin

echo -e "\n=== Checking specifically for unsafe casts (as Type without ?) ==="
# Use simpler pattern to find unsafe casts after onBindViewHolder
rg -B 5 'as\s+\w+\s*$' serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/ --type kotlin | grep -A 5 -B 5 "onBindViewHolder"

Length of output: 2600


🏁 Script executed:

#!/bin/bash
# Description: Find all presenter files and check for unsafe casts pattern

echo "=== Finding all Presenter.kt files in leanback directory ==="
find serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback -name "*Presenter.kt" | sort

echo -e "\n=== Checking for onBindViewHolder with nullable item AND unsafe cast pattern ==="
find serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback -name "*Presenter.kt" -exec grep -l "item:\s*Any?" {} \; | while read file; do
  echo "File: $file"
  rg -A 10 'override fun onBindViewHolder.*item:\s*Any\?' "$file" | head -15
done

Length of output: 784


Unsafe cast will throw exception if item is null.

The parameter item is now nullable (Any?), but line 58 performs an unsafe cast without null checking. If null is passed, this will cause a runtime exception.

Apply this diff to safely handle null items:

-    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
-        val video = item as VideoContentInfo
+    override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
+        val video = item as? VideoContentInfo ?: return

Note: The other presenters mentioned in the original review (CategoryVideoPresenter, EpisodeVideoPresenter, MoviePresenter, SeriesPresenter, VideoContentInfoPresenter) were not found in the leanback directory with nullable item parameters, so this issue appears isolated to CardPresenter.kt.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
val video = item as VideoContentInfo
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any?) {
val video = item as? VideoContentInfo ?: return
🤖 Prompt for AI Agents
In
serenity-app/src/main/java/us/nineworlds/serenity/ui/leanback/search/CardPresenter.kt
around lines 57-58, the code uses an unsafe cast "val video = item as
VideoContentInfo" which will throw if item is null; change to a safe cast and
early-return on null, e.g. use "val video = item as? VideoContentInfo ?: return"
(or handle null by showing a placeholder) so the method tolerates nullable items
and avoids a ClassCastException/NPE.

Comment on lines +4 to +5
import toothpick.Toothpick
import us.nineworlds.serenity.common.annotations.InjectionConstants
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify unused imports.

The imports for Toothpick and InjectionConstants do not appear to be used in this file. Similarly, the javax.inject.Inject import on line 9 appears unused since the resources field was removed.

Run the following script to verify these imports are not used:

If the script shows no matches, consider removing these unused imports.


🏁 Script executed:

#!/bin/bash
# Description: Check if Toothpick, InjectionConstants, or Inject are referenced in this file

# Check for Toothpick usage
echo "=== Checking Toothpick usage ==="
rg -n 'Toothpick\.' serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt

# Check for InjectionConstants usage
echo "=== Checking InjectionConstants usage ==="
rg -n 'InjectionConstants' serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt

# Check for @Inject usage
echo "=== Checking @Inject usage ==="
rg -n '@Inject' serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt

Length of output: 175


🏁 Script executed:

#!/bin/bash
# Get the full file content to verify imports and their usage
cat serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt

Length of output: 3435


Remove unused imports at lines 4, 5, and 9.

The imports for Toothpick, InjectionConstants, and javax.inject.Inject are not referenced anywhere in this file and should be removed:

  • Toothpick (line 4): No usage detected
  • InjectionConstants (line 5): No usage detected
  • javax.inject.Inject (line 9): No @Inject annotations present

These appear to be remnants from the DI refactoring and can be safely removed to clean up the import list.

🤖 Prompt for AI Agents
In
serenity-app/src/main/kotlin/us/nineworlds/serenity/core/model/impl/EpisodeMediaContainer.kt
around lines 4–9, remove the unused imports toothpick.Toothpick (line 4),
us.nineworlds.serenity.common.annotations.InjectionConstants (line 5), and
javax.inject.Inject (line 9); simply delete those import statements so the file
only contains imports that are actually referenced.

Comment on lines +11 to +13
init {
Toothpick.inject(this, Toothpick.openScope(InjectionConstants.APPLICATION_SCOPE))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify: PR summary claims inject call was removed, but it's still present.

The PR summary explicitly states "Fixes unit-test dependency injection issues by removing premature Toothpick.inject call from BaseInjector init block, ensuring test scopes and mock bindings apply before injection." However, the init block still contains the Toothpick.inject call, identical to the Java version's constructor behavior.

If the premature injection causes test failures (as the PR summary suggests), this call should be removed from the init block and injection should be performed manually by subclasses or deferred. Please clarify whether:

  1. The PR summary is inaccurate and the inject call should remain, or
  2. This change is incomplete and the inject call should be removed

Run the following script to check how BaseInjector is used in tests and whether test failures occur:


🏁 Script executed:

#!/bin/bash
# Search for BaseInjector subclasses and test usage patterns
echo "=== BaseInjector subclasses ==="
ast-grep --pattern 'class $_ : BaseInjector'

echo -e "\n=== Test files using BaseInjector subclasses ==="
rg -n --type=kotlin -C3 'BaseInjector|Toothpick\.inject' serenity-app/src/test/

Length of output: 81


I need to broaden the search strategy, as the initial script produced no results. Let me search more comprehensively across the codebase.


🏁 Script executed:

#!/bin/bash
# Broader search for BaseInjector usage and test structure

echo "=== All references to BaseInjector in codebase ==="
rg -n 'BaseInjector' --type=kotlin

echo -e "\n=== Test directory structure ==="
find serenity-app/src -type d -name "*test*" | head -20

echo -e "\n=== Kotlin files in src/main (looking for subclasses) ==="
rg -n 'class.*:.*BaseInjector|class.*extends.*BaseInjector' --type=kotlin

echo -e "\n=== Search for Toothpick usage in codebase ==="
rg -n 'Toothpick\.' --type=kotlin -C2 | head -50

echo -e "\n=== Java files that might be BaseInjector ==="
find . -name "*BaseInjector*" -type f 2>/dev/null

Length of output: 7117


Let me check the Java version and examine test usage to understand the change:


🏁 Script executed:

#!/bin/bash
# Check Java version of BaseInjector
echo "=== Java BaseInjector.java ==="
cat ./serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java

echo -e "\n=== Test files that reference these subclasses ==="
rg -n 'ImageInfographicUtils|AbstractMediaContainer|GalleryOnItemSelectedListener' --type=kotlin serenity-app/src/test/

echo -e "\n=== All test files in serenity-app ==="
find serenity-app/src/test -name "*.kt" -o -name "*.java" | head -20

Length of output: 2562


Let me examine the test files and the test base class to understand the injection problem:


🏁 Script executed:

#!/bin/bash
# View test base class InjectingTest
echo "=== InjectingTest base class ==="
find serenity-app/src/test -name "InjectingTest.kt" -o -name "InjectingTest.java" | xargs cat

echo -e "\n=== ImageInfographicUtilsTest ==="
cat serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/util/ImageInfographicUtilsTest.kt

echo -e "\n=== Check if BaseInjector.java exists and has content ==="
ls -la ./serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java
wc -l ./serenity-app/src/main/java/us/nineworlds/serenity/injection/BaseInjector.java 2>/dev/null || echo "File not found or error reading"

echo -e "\n=== Recent git changes to BaseInjector ==="
git log --oneline -n 20 -- "*BaseInjector*" 2>/dev/null | head -20

Length of output: 19396


Remove Toothpick.inject call from BaseInjector init block—PR summary claims it was removed but it's still present.

The PR summary explicitly states "Fixes unit-test dependency injection issues by removing premature Toothpick.inject call from BaseInjector init block," yet the init block at lines 11-13 still contains the identical call. This causes a critical test failure pattern:

  1. Test setup (InjectingTest) opens APPLICATION_SCOPE, installs test modules with mocks, then injects
  2. Test instantiates ImageInfographicUtils(100, 100), which triggers BaseInjector's init block
  3. Init block creates a new APPLICATION_SCOPE via Toothpick.openScope() and injects into it
  4. This new scope has no test modules installed, so mocks never apply to the subclass instance

The Kotlin conversion must complete the intended fix by removing the inject call from the init block. Subclasses should handle injection explicitly via test setup or factory methods, not automatically during construction.

🤖 Prompt for AI Agents
In serenity-app/src/main/kotlin/us/nineworlds/serenity/injection/BaseInjector.kt
around lines 11 to 13, the init block still calls Toothpick.inject(this,
Toothpick.openScope(InjectionConstants.APPLICATION_SCOPE)); remove this
Toothpick.inject call from the init block so construction no longer
opens/creates and injects into a new APPLICATION_SCOPE; ensure subclasses
receive injection only via explicit calls in tests or factories (e.g., let test
setup call Toothpick.inject on the already-opened scope) and update any
constructors or factory code to perform injection where needed instead of
automatic injection in BaseInjector.

Comment on lines +22 to 23
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
val video = item as VideoContentInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Add null-safety check before casting nullable item.

The item parameter is now nullable (Any?) but is immediately cast to VideoContentInfo without a null check. If the Leanback framework passes null to onBindViewHolder, this will throw a NullPointerException or TypeCastException.

Consider adding a null check or using a safe cast:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
+    if (item == null) return
     val video = item as VideoContentInfo

Or use a safe cast with early return:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
-    val video = item as VideoContentInfo
+    val video = item as? VideoContentInfo ?: return

Please verify whether the AndroidX Leanback library can actually pass null values to onBindViewHolder in this version:


🏁 Script executed:

#!/bin/bash
# Search for other presenter implementations and their null-handling patterns
rg -n "override fun onBindViewHolder" --type kotlin -A 2

Length of output: 3708


I need to verify whether the AndroidX Leanback framework actually passes null values to onBindViewHolder. This is critical to determine if the issue is valid.


🌐 Web query:

AndroidX Leanback Presenter onBindViewHolder null parameter

💡 Result:

Short answer: The Presenter.onBindViewHolder item parameter is @nullable — it can be null. Always null-check before casting or assume item may be missing (this is by-design for some presenters/rows). Handle null (skip bind / use default state) or ensure your ObjectAdapter never contains nulls or you use a different Presenter/PresenterSelector for that row. [1][2]

Useful links:

  • Presenter API (shows onBindViewHolder signature with @nullable item). [1]
  • ItemBridgeAdapter / Presenter usage (explains how presenters are bound to adapter items). [2]

References:
[1] AndroidX Presenter API.
[2] AndroidX ItemBridgeAdapter / Presenter docs.


Add null-safety check before casting nullable item.

The item parameter is nullable (Any?) but is immediately cast to VideoContentInfo without a null check. The AndroidX Leanback framework can pass null to onBindViewHolder, and casting null will throw a NullPointerException or TypeCastException at runtime.

This pattern affects multiple presenters in the codebase (CategoryVideoPresenter, MoviePresenter, SeriesPresenter, CardPresenter), all of which have the same vulnerability.

Add a null check before casting:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
+    if (item == null) return
     val video = item as VideoContentInfo

Or use a safe cast:

 override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
-    val video = item as VideoContentInfo
+    val video = item as? VideoContentInfo ?: return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
val video = item as VideoContentInfo
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any?) {
val video = item as? VideoContentInfo ?: return
🤖 Prompt for AI Agents
In
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/leanback/presenters/VideoContentInfoPresenter.kt
around lines 22-23, the nullable parameter item is being unguardedly cast to
VideoContentInfo which can throw at runtime; add a null-safety guard by either
returning early when item is null or using a safe cast (item as?
VideoContentInfo) and handle the null case (e.g., return or skip binding),
ensuring subsequent code uses the non-null video variable.

@codecov
Copy link

codecov bot commented Nov 17, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.51%. Comparing base (91fcf80) to head (c052054).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
...ty/ui/leanback/presenters/EpisodeVideoPresenter.kt 0.00% 2 Missing ⚠️
...neworlds/serenity/ui/util/ImageInfographicUtils.kt 66.66% 0 Missing and 1 partial ⚠️
...lds/serenity/ui/video/player/ExoplayerPresenter.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #484      +/-   ##
=============================================
+ Coverage      47.12%   47.51%   +0.39%     
+ Complexity       446      445       -1     
=============================================
  Files             59       59              
  Lines           1895     1892       -3     
  Branches         259      255       -4     
=============================================
+ Hits             893      899       +6     
  Misses           906      906              
+ Partials          96       87       -9     
Flag Coverage Δ
unittests 47.51% <50.00%> (+0.39%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant