Skip to content

feat: support sdui for video storage#21

Merged
tejpratap46 merged 2 commits intomainfrom
feat/implement-sdui
Feb 23, 2026
Merged

feat: support sdui for video storage#21
tejpratap46 merged 2 commits intomainfrom
feat/implement-sdui

Conversation

@tejpratap46
Copy link
Owner

@tejpratap46 tejpratap46 commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Added project management system with project creation, viewing, and sharing capabilities
    • Album art fetching now uses repository pattern for better reliability
    • Enhanced lyrics syncing with interactive drag-based selector
    • Added project cancellation support for video workers
  • Improvements

    • Updated build toolchain to Java 21 and Gradle 9.2.1
    • Refactored configuration management for improved stability
    • Enhanced notification and lifecycle handling in background tasks
  • Chores

    • Removed legacy API clients and replaced with modular architecture
    • Updated multiple framework dependencies

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

This PR performs a comprehensive refactoring upgrading the Java/Kotlin build to version 21, introduces a new motion-store module with database persistence for projects, refactors configuration management from static mutable to immutable provider-based approach, restructures album art and lyrics APIs to use repository pattern, makes worker lifecycle methods suspend-friendly, and significantly expands UI with project management and interactive lyric selection.

Changes

Cohort / File(s) Summary
Build System & Gradle
.idea/compiler.xml, .idea/gradle.xml, .idea/misc.xml, gradle.gradle.kts, gradle.properties, gradle/libs.versions.toml, gradle/wrapper/gradle-wrapper.properties, settings.gradle
Updated Java target level from 17 to 21, bumped Gradle from 8.13 to 9.2.1, updated multiple dependency versions (Kotlin 2.2.21→2.3.10, AGP 8.13.2→9.0.1, etc.), added motion-store module to project includes, and modified Gradle build properties for Android features.
MotionConfig Architecture Refactoring
modules/core/src/main/java/com/tejpratapsingh/motionlib/core/MotionConfig.kt, modules/*/src/main/java/.../Filament3dView.kt, modules/*/src/main/java/.../MotionOpenGlView.kt, modules/*/src/main/java/.../ContourDevice.kt, modules/*/src/main/java/.../RenaultCar.kt, modules/*/src/main/java/.../RenaultCar.kt, modules/*/src/main/java/.../BaseFrameMotionView.kt, modules/*/src/main/java/.../MotionComposerView.kt, modules/*/src/main/java/.../GradientView.kt, modules/*/src/main/java/.../RotatingMotionView.kt, modules/*/src/main/java/.../VideoFrameView.kt, modules/motionlib/src/main/java/.../MotionVideoProducer.kt, modules/motionlib/ui/MotionVideoPlayer.kt
Converted MotionConfig from mutable singleton to immutable data class with setCurrentConfig/provideCurrentConfig provider functions; replaced all static MotionConfig references with dynamic provideCurrentConfig() calls across render views, composition views, and playback components.
New Motion-Store Module
modules/motion-store/.gitignore, modules/motion-store/build.gradle, modules/motion-store/proguard-rules.pro, modules/motion-store/src/main/AndroidManifest.xml, modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt, modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/dao/BaseDao.kt, modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/dao/MotionProjectDao.kt, modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/infra/DatabaseManager.kt, modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/extensions/ContextExtensions.kt, modules/motion-store/src/test/java/.../ExampleUnitTest.kt
Introduces new library module with SQLite-backed database persistence, MotionProject data model, generic BaseDao CRUD operations, MotionProjectDao schema/mapping, DatabaseManager lifecycle handling, and context extensions for project file creation.
Album Art Feature Refactoring
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSource.kt, modules/lyrics-maker/src/main/java/.../AlbumArtRemoteDataSourceImpl.kt, modules/lyrics-maker/src/main/java/.../AlbumArtRepository.kt, modules/lyrics-maker/src/main/java/.../AlbumArtRepositoryImpl.kt, modules/lyrics-maker/src/main/java/.../albumart/data/MusicBrainzResponse.kt, modules/lyrics-maker/src/main/java/.../albumart/data/Recording.kt, modules/lyrics-maker/src/main/java/.../albumart/data/Release.kt, modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/client/AlbumArtFetcher.kt
Replaced monolithic AlbumArtFetcher singleton with repository pattern: new remote data source interface/impl for MusicBrainz API calls, album art repository interface/impl as abstraction, and structured data models (MusicBrainzResponse, Recording, Release) for API responses.
Lyrics API Refactoring
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiService.kt, modules/lyrics-maker/src/main/java/.../LrcLibApiServiceImpl.kt, modules/lyrics-maker/src/main/java/.../LyricsRepository.kt, modules/lyrics-maker/src/main/java/.../LyricsRepositoryImpl.kt, modules/lyrics-maker/src/main/java/.../lrclib/model/GetParams.kt, modules/lyrics-maker/src/main/java/.../lrclib/model/SearchParams.kt, modules/lyrics-maker/src/main/java/.../lrclib/model/LyricsResponse.kt, modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/di/OkHttpProvider.kt, modules/lyrics-maker/src/main/java/.../data/api/client/LrcLibClient.kt, modules/lyrics-maker/src/main/java/.../data/api/model/LyricsQuery.kt, modules/lyrics-maker/src/main/java/.../data/api/model/SearchQuery.kt
Replaced LrcLibClient singleton with service/repository pattern; introduced LrcLibApiService/Impl for HTTP calls, LyricsRepository/Impl as abstraction, structured param models (GetParams, SearchParams), OkHttpProvider for shared HTTP client, and updated LyricsResponse with Gson @SerializedName annotations; removed legacy query models.
Worker & Adapter Suspend Refactoring
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/VideoProducerAdapter.kt, modules/motionlib/src/main/java/.../adapter/AndroidVideoProducerAdapter.kt, modules/motionlib/src/main/java/.../core/infra/AndroidVideoGenerator.kt, modules/motionlib/worker/MotionWorker.kt, modules/lyrics-maker/src/main/java/.../worker/LyricsMotionWorker.kt, modules/app/src/main/java/.../worker/SampleMotionWorker.kt, modules/ffmpeg-motion-ext/src/main/java/.../FfmpegVideoProducerAdapter.kt, modules/jcodec-motion-ext/src/main/java/.../JCodecVideoProducerAdapter.kt
Removed motionConfig parameter from VideoProducerAdapter/implementations and added runtime retrieval via provideCurrentConfig(); made progressListener suspendable; introduced suspend getOutputFile(), onProgress, onCompleted in MotionWorker and implementations; updated worker lifecycle to use applicationContext consistently.
Kotlin Android Plugin Removal & Publishing
modules/*/build.gradle
Removed kotlin.android plugin alias and kotlinOptions blocks across all library/app modules (3d-filament-renderer, 3d-opengl-renderer, app, core, ffmpeg-motion-ext, ivi-demo, jcodec-motion-ext, lyrics-maker, metadata-extractor, motionlib, pytorch-motion-ext, sdui, templates, tensorflow-motion-ext); replaced with explicit publishing { singleVariant("release") } blocks.
SDUI Config & Action Handler Refactoring
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/data/SduiRenderer.kt, modules/sdui/src/main/java/.../domain/ActionHandler.kt, modules/sdui/src/main/java/.../presentation/DefaultActionHandler.kt, modules/sdui/src/main/java/.../infra/ConfigParser.kt, modules/sdui/src/main/java/.../infra/VideoAspectRatioParser.kt, modules/sdui/src/main/java/.../infra/MotionEffectParser.kt, modules/sdui/src/main/java/.../infra/MotionViewParser.kt
Removed ActionHandler interface and DefaultActionHandler implementation from SduiRenderer; added JSON serialization/deserialization extensions for MotionConfig, VideoAspectRatio, MotionEffect, and MotionView via Gson.
Lyrics UI & ViewModel Refactoring
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt, modules/lyrics-maker/src/main/java/.../activity/SearchActivity.kt, modules/lyrics-maker/src/main/java/.../compose/AppNavHost.kt, modules/lyrics-maker/src/main/java/.../compose/SearchLyricsCompose.kt, modules/lyrics-maker/src/main/java/.../compose/SyncedLyricsSelector.kt, modules/lyrics-maker/src/main/java/.../viewmodel/LyricsViewModel.kt, modules/lyrics-maker/src/main/res/layout/lyrics_container.xml
Refactored LyricsViewModel to use repository-backed searchLyrics with LyricsUiState (Initial/Loading/Success/Error), updated Compose screens to navigate via AppNavHost with ProjectsViewModel integration, replaced single-click lyric selection with comprehensive drag-based interactive SyncedLyricsSelector UI, and adjusted layout sizing/visibility.
Projects Management UI & ViewModel
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/ProjectsScreenCompose.kt, modules/lyrics-maker/src/main/java/.../viewmodel/ProjectsViewModel.kt, modules/lyrics-maker/src/main/AndroidManifest.xml, modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/LyricsApp.kt, modules/lyrics-maker/src/main/java/.../worker/LyricsMotionWorkerCancelReceiver.kt, modules/lyrics-maker/src/main/res/values/strings.xml
Introduced ProjectsRoute/Screen Compose UI with grid display, pull-to-refresh, project cards with video thumbnails, and delete/share actions; added ProjectsViewModel with StateFlow-backed project list and UI events; integrated LyricsApp application class with database/motion-store DAOs; added work cancellation receiver for user-initiated job cancellation.
Configuration & Motion Project Setup
modules/app/src/main/java/com/tejpratapsingh/animator/presentation/SampleMotionVideo.kt, modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/MainActivity.kt, modules/ivi-demo/src/main/java/.../sequence/RenaultSequence.kt, modules/lyrics-maker/src/main/java/.../presentation/motion/LyricsVideoProducer.kt, modules/lyrics-maker/src/main/java/.../presentation/motion/MultiLyricsVideoProducer.kt
Updated video producer initialization to call setCurrentConfig before builder creation instead of passing config parameter; integrated provideCurrentProject for project context setup in motion producers.
Metadata Extractor Updates
modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt, modules/metadata-extractor/src/main/res/layout/activity_share_receiver.xml
Added URL extraction logic to ShareReceiverActivity, improved logging for link discovery, added UI affordances for IME actions and EditorActionListener, constrained progress indicator to parent top, and enabled single-line capitalized text input for title field.
Miscellaneous Cleanup & Logging
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/animation/MotionInterpolator.kt, modules/motionlib/src/main/java/.../effects/SlideRightToLeftEffect.kt
Removed debug logging from MotionInterpolator interpolateForRange and removed unused TAG field from SlideRightToLeftEffect.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Mobile App
    participant LyricsVM as LyricsViewModel
    participant Repo as LyricsRepository
    participant Service as LrcLibApiService
    participant HTTP as OkHttp Client
    participant API as LrcLib API

    Client->>LyricsVM: searchLyrics(trackName, artistName)
    LyricsVM->>LyricsVM: uiState = Loading
    LyricsVM->>Repo: searchLyrics(SearchParams)
    Repo->>Service: search(SearchParams)
    Service->>HTTP: GET /search?track_name=X&artist_name=Y
    HTTP->>API: HTTP Request
    API-->>HTTP: JSON Response
    HTTP-->>Service: Response Body
    Service->>Service: Parse JSON via Gson
    Service-->>Repo: Result.success(List<LyricsResponse>)
    Repo-->>LyricsVM: Result.success(lyrics)
    LyricsVM->>LyricsVM: uiState = Success(lyrics)
    LyricsVM-->>Client: StateFlow emits updated state
    Client->>Client: Render lyrics list
Loading
sequenceDiagram
    participant App as LyricsApp
    participant DBMgr as DatabaseManager
    participant DB as SQLite
    participant DAO as MotionProjectDao
    participant Store as ProjectsViewModel

    App->>DBMgr: init(context, schemas=[MotionProject])
    DBMgr->>DB: onCreate(db)
    DB->>DB: Execute CREATE TABLE MotionProject
    Store->>DAO: loadProjects()
    DAO->>DB: query(tableName, selection, args)
    DB-->>DAO: Cursor<MotionProject>
    DAO->>DAO: fromCursor() for each row
    DAO-->>Store: List<MotionProject>
    Store->>Store: _projects.emit(list)
    Store-->>UI: StateFlow<List<MotionProject>>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • fix: update lyrics layout #19: Modifies lyrics UI layout (lyrics_container.xml) and LyricsContainer view code, indicating related UI/layout changes to the lyrics display feature.
  • fix: update module to mvvm #15: Updates metadata-extractor module API and ShareReceiverActivity with MVVM refactoring patterns similar to the lyrics-maker ViewModel/Repository changes in this PR.

Suggested labels

enhancement, refactoring, database, architecture

Suggested reviewers

  • hemusimple

Poem

🐰 Hoppy refactoring spree, providers dance with glee,
Config flows dynamic, no more static decree!
Database blooms anew, repositories too,
Workers suspend with care, motion projects renew.
Gradle ascends to twenty-one, lyric UI shines—
A rabbit's delight in these architectural lines! 🎬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: support sdui for video storage' aligns with the primary objective of adding SDUI serialization/deserialization support and a new motion-store module for persisting MotionProject data.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/implement-sdui

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @tejpratap46, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a substantial overhaul of the project's foundational components and a targeted refactoring of the 'lyrics-maker' module. The core build system and language environment have been modernized with upgrades to Gradle, Android Gradle Plugin, Kotlin, and JDK 21, setting the stage for future development. A new 'motion-store' module introduces local data persistence capabilities, while the MotionConfig system has been made more flexible. The 'lyrics-maker' module benefits from a complete rewrite of its network layer, adopting a more maintainable and scalable architecture. These changes collectively improve the project's stability, performance, and development experience.

Highlights

  • Project Infrastructure Upgrade: The project has undergone a significant upgrade, moving to Gradle 9.1.0, Android Gradle Plugin 9.0.0, Kotlin 2.3.0, and targeting JDK 21. This ensures compatibility with the latest development tools and language features.
  • New 'motion-store' Module: A new module, 'modules/motion-store', has been introduced to provide local data persistence for motion projects, utilizing the Nitrite NoSQL database.
  • Refactored MotionConfig Management: The MotionConfig class in 'modules/core' has been refactored from a singleton 'data object' to a 'data class' with a dedicated 'provideMotionConfig()' function, enhancing its flexibility and testability.
  • Lyrics Maker Module Rework: The 'lyrics-maker' module's API clients for fetching album art and lyrics have been completely re-architected. This involves a migration from Ktor to OkHttp and Gson, and the implementation of a more robust repository and service pattern for improved maintainability and separation of concerns.
  • Streamlined Build Configurations: The 'kotlin-android' plugin and explicit 'kotlinOptions.jvmTarget' configurations have been removed from numerous module 'build.gradle' files, simplifying build scripts and leveraging default AGP Kotlin compilation.
  • SDUI Configuration Serialization: The 'modules/sdui' now includes utility functions for serializing and deserializing MotionConfig and VideoAspectRatio objects to and from JSON, enabling dynamic configuration of UI elements.
Changelog
  • .idea/compiler.xml
    • Updated 'bytecodeTargetLevel' from 17 to 21.
  • .idea/gradle.xml
    • Added 'modules/motion-store' to included modules.
  • .idea/misc.xml
    • Updated 'languageLevel' and 'project-jdk-name' from JDK_17 to JDK_21.
  • build.gradle
    • Renamed to 'build.gradle.kts'.
    • Updated 'ktlint' dependency syntax.
  • gradle.properties
    • Added several new Android build feature flags.
  • gradle/libs.versions.toml
    • Updated various dependency versions (Filament, Jsoup, Ktor, Work, AGP, Kotlin, Activity, Compose, Navigation Compose, Runtime).
    • Added Nitrite DB dependency.
  • gradle/wrapper/gradle-wrapper.properties
    • Updated Gradle distribution URL from 8.13 to 9.1.0.
  • modules/3d-filament-renderer/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/3d-filament-renderer/src/main/java/com/tejpratapsingh/motionlib/filamentrenderer/Filament3dView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/3d-opengl-renderer/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/3d-opengl-renderer/src/main/java/com/tejpratapsingh/motionlib/openglrenderer/MotionOpenGlView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/app/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/app/src/main/java/com/tejpratapsingh/animator/ui/view/ContourDevice.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/app/src/main/java/com/tejpratapsingh/animator/ui/view/RenaultCar.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/core/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'kotlinOptions.jvmTarget'.
    • Added 'gson' dependency.
  • modules/core/src/main/java/com/tejpratapsingh/motionlib/core/MotionConfig.kt
    • Refactored 'MotionConfig' from 'data object' to 'data class'.
    • Introduced 'provideMotionConfig()' function for configuration access.
  • modules/ffmpeg-motion-ext/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/ivi-demo/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/RenaultCar.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/Road.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/jcodec-motion-ext/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/lyrics-maker/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSource.kt
    • Added new interface for album art remote data source.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt
    • Added new implementation for album art remote data source using OkHttp.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRepository.kt
    • Added new interface for album art repository.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRepositoryImpl.kt
    • Added new implementation for album art repository.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/data/MusicBrainzResponse.kt
    • Added new data class for MusicBrainz API response.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/data/Recording.kt
    • Added new data class for MusicBrainz Recording.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/data/Release.kt
    • Added new data class for MusicBrainz Release.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/client/AlbumArtFetcher.kt
    • Removed old Ktor-based album art fetcher.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/client/LrcLibClient.kt
    • Removed old Ktor-based LrcLib client.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiService.kt
    • Added new interface for LrcLib API service.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt
    • Added new implementation for LrcLib API service using OkHttp and Gson.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LyricsRepository.kt
    • Added new interface for lyrics repository.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LyricsRepositoryImpl.kt
    • Added new implementation for lyrics repository.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/GetParams.kt
    • Added new data class for LrcLib Get parameters.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/SearchParams.kt
    • Added new data class for LrcLib Search parameters.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/model/LyricsQuery.kt
    • Removed old LyricsQuery data class.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/model/LyricsResponse.kt
    • Renamed to 'modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt'.
    • Updated 'LyricsResponse' data class with Gson annotations and 'Double' for duration.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/model/SearchQuery.kt
    • Removed old SearchQuery data class.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/di/OkHttpProvider.kt
    • Added new OkHttp client provider.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt
    • Added Toast for UI state feedback.
    • Updated lyrics fetching logic to use new ViewModel methods and 'uiState' flow.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/AppNavHost.kt
    • Updated 'onLyricsSelected' to use 'viewModel.selectedLyric.tryEmit(it)'.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt
    • Updated UI to react to 'LyricsUiState' flow.
    • Removed 'isLoading' and 'lyricsList' direct usage.
    • Updated search logic.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt
    • Updated album art fetching to use new repository pattern.
    • Removed 'ProgressBar' and 'LinearLayoutCompat' visibility.
  • modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt
    • Reworked ViewModel to use new API service and repository.
    • Introduced 'LyricsUiState' sealed class for better state management.
    • Removed 'selectedLyricResponse' and 'lyricsList' direct usage.
  • modules/lyrics-maker/src/main/res/layout/lyrics_container.xml
    • Changed text size.
    • Hid seek bar and button container.
    • Adjusted 'fake_chart_view' constraints.
  • modules/metadata-extractor/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/motion-store/.gitignore
    • Added new gitignore for the module.
  • modules/motion-store/build.gradle
    • Added new build file for the 'motion-store' module, including Nitrite DB dependency.
  • modules/motion-store/proguard-rules.pro
    • Added new ProGuard rules for the module.
  • modules/motion-store/src/androidTest/java/com/tejpratapsingh/motionstore/ExampleInstrumentedTest.kt
    • Added new Android instrumented test.
  • modules/motion-store/src/main/AndroidManifest.xml
    • Added new Android manifest.
  • modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/DatabaseProvider.kt
    • Added new class for Nitrite DB provider.
  • modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt
    • Added new data class for 'MotionProject' table.
  • modules/motion-store/src/test/java/com/tejpratapsingh/motionstore/ExampleUnitTest.kt
    • Added new unit test.
  • modules/motionlib/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'targetSdk' from 'defaultConfig'.
    • Added 'lint', 'testOptions', and 'publishing' blocks.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/animation/MotionInterpolator.kt
    • Removed 'Log' and 'BuildConfig.DEBUG' usage.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseContourMotionView.kt
    • Added 'MotionConfig' import.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionComposerView.kt
    • Added 'config' parameter to constructor and used it for layout dimensions.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionVideoProducer.kt
    • Removed direct 'MotionConfig' assignments and passed 'config' to 'MotionComposerView'.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/background/GradientView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
    • Removed commented out 'valueRange' logic.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/container/RotatingMotionView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/video/VideoFrameView.kt
    • Replaced direct 'MotionConfig' access with 'provideMotionConfig()'.
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SlideRightToLeftEffect.kt
    • Removed unused 'TAG' variable.
  • modules/pytorch-motion-ext/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/sdui/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/data/SduiRenderer.kt
    • Removed 'actionHandler' from constructor.
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/domain/ActionHandler.kt
    • Removed.
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/ConfigParser.kt
    • Added new utility functions for MotionConfig serialization/deserialization.
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt
    • Added new utility functions for VideoAspectRatio serialization/deserialization.
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/presentation/DefaultActionHandler.kt
    • Removed.
  • modules/templates/build.gradle
    • Removed 'kotlin.android' plugin.
    • Removed 'targetSdk' from 'defaultConfig'.
    • Added 'lint', 'testOptions', and 'publishing' blocks.
    • Removed 'kotlinOptions.jvmTarget'.
  • modules/tensorflow-motion-ext/build.gradle
    • Removed 'kotlin.android' plugin.
    • Added 'publishing' block.
    • Removed 'kotlinOptions.jvmTarget'.
  • settings.gradle
    • Included new 'modules:motion-store'.
Activity
  • No human activity (comments, reviews, or explicit progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@amazon-q-developer amazon-q-developer bot left a comment

Choose a reason for hiding this comment

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

Critical Issues Found

This PR introduces SDUI support for video storage but contains 8 blocking defects that must be fixed before merge:

Security (1)

  • Hardcoded database credentials in DatabaseProvider expose no-op authentication

Crash Risks (3)

  • Resource leaks in LrcLibApiServiceImpl will cause OutOfMemoryError
  • DatabaseProvider constructor prevents instantiation, making module unusable
  • Invalid Gradle syntax in motion-store build.gradle will fail compilation

Logic Errors (2)

  • MotionConfig race condition breaks multi-instance configurations
  • Connection pool misconfiguration destroys HTTP performance benefits

Summary

The refactoring from data object to data class for MotionConfig and the migration from Ktor to OkHttp are architecturally sound, but implementation issues block merge. Focus on fixing the resource leaks and database provider instantiation first as these cause runtime crashes.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.


⚠️ This PR contains more than 30 files. Amazon Q is better at reviewing smaller PRs, and may miss issues in larger changesets.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant refactoring and new features. Key changes include upgrading Gradle and Kotlin versions, refactoring MotionConfig to be instance-based rather than a singleton object, and replacing Ktor with OkHttp/Gson in the lyrics-maker module along with a repository pattern. A new motion-store module for database persistence using Nitrite and an sdui module for server-driven UI have been added. The changes are generally positive, improving code structure and updating dependencies. My review focuses on potential issues like thread blocking, incomplete implementations in new modules, and making the code more robust and maintainable.

Copy link

@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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (15)
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseContourMotionView.kt (1)

39-45: ⚠️ Potential issue | 🟡 Minor

Pre-existing off-by-one: 0..this.childCount iterates one index too far.

Not introduced by this PR, but 0..childCount is an inclusive range in Kotlin, so getChildAt(childCount) returns null. The is MotionView check prevents a crash, but it's semantically incorrect. The idiomatic fix is 0 until childCount.

Suggested fix
-        for (i in 0..this.childCount) {
+        for (i in 0 until this.childCount) {
modules/pytorch-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Do not remove the kotlin-android plugin; this module contains Kotlin source files.

The module has 9 Kotlin source files (including PyTorchImageProcessor.kt, ModelTypes.kt, RemoveBg.kt, and others), but the build.gradle removes the kotlin-android plugin. This will cause compilation failures. Either retain the Kotlin plugin or convert all source files to Java.

modules/jcodec-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore the Kotlin Android plugin—this module has .kt source files that will fail to compile without it.

The kotlin.android plugin was removed but the module contains three Kotlin source files:

  • src/main/java/com/tejpratapsingh/motionlib/jcodec/FileExtension.kt
  • src/main/java/com/tejpratapsingh/motionlib/jcodec/JCodecVideoProducerAdapter.kt
  • src/test/java/com/tejpratapsingh/motionlib/jcodec/ExampleUnitTest.kt

Without the Kotlin plugin, these files cannot be compiled.

modules/sdui/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Add the Kotlin Android plugin back to the build.gradle file.

Multiple Kotlin source files exist in the module (10 .kt files including SduiRenderer.kt, ViewFactory.kt, ConfigParser.kt, ContainerFactory.kt, ImageFactory.kt, TextFactory.kt, ImageLoader.kt, VideoAspectRatioParser.kt, and test files), but the Kotlin Android plugin has been removed. These files will not compile without it.

modules/tensorflow-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Add the Kotlin plugin back to build.gradle—the module contains 7 Kotlin source files that cannot compile without it.

The module has 6 Kotlin sources in src/main/java/ and 1 test source in src/test/java/. Removing the Kotlin Android plugin will break the build. Either restore the Kotlin plugin or migrate all Kotlin files to Java.

modules/3d-opengl-renderer/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Remove kotlin-android plugin must be restored or all Kotlin sources removed.

This module contains 5 Kotlin source files:

  • MotionOpenGlView.kt
  • ObjModel.kt
  • Object3DToBitmapRenderer.kt
  • OffscreenRenderer.kt
  • ExampleUnitTest.kt

Removing the kotlin-android plugin will cause compilation to fail. Either restore the plugin or migrate these files to Java.

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionComposerView.kt (1)

35-36: ⚠️ Potential issue | 🟡 Minor

Pre-existing off-by-one: 0..this.childCount should be 0 until this.childCount.

The .. range is inclusive, so getChildAt(childCount) returns null, which won't crash here (the is MotionView check handles it), but iterating one extra index is unnecessary.

Proposed fix
-        for (i in 0..this.childCount) {
+        for (i in 0 until this.childCount) {
modules/ffmpeg-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore the kotlin-android plugin—this module contains Kotlin source files.

The build will fail to compile the Kotlin sources in this module:

  • src/main/java/com/tejpratapsingh/motionlib/ffmpeg/FfmpegVideoProducerAdapter.kt
  • src/main/java/com/tejpratapsingh/motionlib/ffmpeg/utils/FFMpegExtensions.kt
  • src/main/java/com/tejpratapsingh/motionlib/ffmpeg/video/FFMpegVideoFrameView.kt
  • src/test/java/com/tejpratapsingh/motionlib/ffmpeg/ExampleUnitTest.kt

Add kotlin-android back to the plugins block.

modules/3d-filament-renderer/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Re-add the kotlin-android plugin to build.gradle.

This module contains Kotlin source files (Filament3dView.kt, FilamentOffscreenCapturer.kt, ExampleUnitTest.kt). Removing the alias(libs.plugins.kotlin.android) plugin will prevent these files from compiling. The plugin must be re-added.

modules/motionlib/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Remove the kotlin.android plugin from the published build configuration at your own risk.

This module contains 33 Kotlin source files. Removing the kotlin.android plugin will break compilation of all .kt files in the module. Restore alias(libs.plugins.kotlin.android) to the plugins block unless the entire codebase is being migrated to Java.

modules/templates/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

The kotlin-android plugin should not be removed: the module contains Kotlin sources.

modules/templates/src/test/java/com/tejpratapsingh/motionlib/templates/ExampleUnitTest.kt exists in this module. Removing the kotlin-android plugin while Kotlin test files are present will prevent proper Kotlin compilation. Either restore the plugin or ensure Kotlin compilation is handled through an alternative mechanism.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt (1)

93-93: ⚠️ Potential issue | 🟠 Major

Side-effect in composition: onSelectionChanged called during recomposition.

onSelectionChanged(selected) on line 93 is invoked directly inside the composable body, meaning it fires on every recomposition when selection != null. Side-effects in Compose should use effect handlers like LaunchedEffect to avoid repeated/unintended invocations.

Proposed fix
-        if (selection != null) {
-            val selected = viewModel.lyrics.subList(selection!!.minIndex, selection!!.maxIndex + 1)
+        val selected = selection?.let {
+            viewModel.lyrics.subList(it.minIndex, it.maxIndex + 1)
+        }
+        if (selected != null) {
             Surface(tonalElevation = 2.dp) {
                 ...
             }
             HorizontalDivider()
-            onSelectionChanged(selected)
         }
+
+        LaunchedEffect(selection) {
+            if (selected != null) {
+                onSelectionChanged(selected)
+            }
+        }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

79-100: ⚠️ Potential issue | 🟠 Major

runBlocking inside a View's init blocks the main thread and risks ANR.

Network calls (album art URL fetch + bitmap download) are performed synchronously on the main thread via runBlocking. This can freeze the UI for several seconds or trigger an Application Not Responding dialog. Consider launching a coroutine on a background scope and posting the bitmap back to the view.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt (1)

91-180: ⚠️ Potential issue | 🟠 Major

Error state is silently swallowed — users get no feedback on search failure.

The when block only matches Success explicitly; LyricsUiState.Error falls through to the else branch, which just shows recent searches. When the API call fails, the loading spinner vanishes and the user sees no indication of what went wrong.

Consider handling Error explicitly:

Proposed fix
         when (val state = uiState.value) {
             is LyricsUiState.Success -> {
                 // ... existing success rendering ...
             }
 
-            else -> {
+            is LyricsUiState.Error -> {
+                Text(
+                    text = state.message,
+                    color = MaterialTheme.colorScheme.error,
+                    style = MaterialTheme.typography.bodyMedium,
+                    modifier = Modifier.padding(vertical = 8.dp),
+                )
+                // fall through to show recent searches below
+            }
+
+            else -> {}
+        }
+
+        if (uiState.value !is LyricsUiState.Success) {
                 Spacer(modifier = Modifier.height(16.dp))
                 Text("Recent Searches:", style = MaterialTheme.typography.titleMedium)
-                LazyColumn {
+                LazyColumn(modifier = Modifier.fillMaxSize()) {
                     items(recentSearches.value.size) { idx ->
                         // ... existing recent searches rendering ...
                     }
                 }
-            }
         }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

103-113: ⚠️ Potential issue | 🔴 Critical

selectedLyrics getter crashes with NoSuchElementException when the list is empty.

field defaults to emptyList(), and line 105 calls field.first() unconditionally. Any access to selectedLyrics before it is explicitly set (or if set to an empty list) will crash at runtime.

Proposed fix
     var selectedLyrics: List<SyncedLyricFrame> = emptyList()
         get() {
+            if (field.isEmpty()) return emptyList()
             val firstFrame = field.first().frame
             return field
                 .map {
🤖 Fix all issues with AI agents
In @.idea/compiler.xml:
- Line 4: The IDE bytecodeTargetLevel is set to "21" but module
compileOptions/sourceCompatibility and targetCompatibility are still
JavaVersion.VERSION_11; update one side so they match. Either change the
<bytecodeTargetLevel target="21"> entry to target="11" to match existing
compileOptions/sourceCompatibility and targetCompatibility, or update all module
build.gradle settings (sourceCompatibility/targetCompatibility and any
compileOptions.release) to Java 21 so Gradle and the IDE are consistent; adjust
the corresponding symbol(s) bytecodeTargetLevel and the Gradle properties
sourceCompatibility/targetCompatibility/compileOptions.release accordingly.

In
`@modules/3d-filament-renderer/src/main/java/com/tejpratapsingh/motionlib/filamentrenderer/Filament3dView.kt`:
- Line 116: The aspect ratio calculation uses integer division because
provideMotionConfig().aspectRatio.width and .height are Ints, causing a zero or
wrong ratio; change the expression in Filament3dView (where you call
setProjection) to perform floating-point division by converting width and/or
height to Double before the division (e.g., cast the numerator or both sides to
Double or multiply by 1.0) so the resulting aspect ratio is a proper Double.

In
`@modules/core/src/main/java/com/tejpratapsingh/motionlib/core/MotionConfig.kt`:
- Around line 3-11: The MotionConfig data class currently mutates global state
in its init block and uses mutable properties; remove the init block that writes
MotionConfigStore.motionConfig, change the properties in MotionConfig
(aspectRatio, fps, outputQuality) from var to val to make the config immutable,
and add/use an explicit registration API on MotionConfigStore (e.g.,
register(config: MotionConfig) or setMotionConfig(config: MotionConfig)) so
callers must explicitly call MotionConfigStore.register(...) after constructing
or copying a config; update any call sites that relied on implicit registration
to call the new register method.
- Around line 13-17: MotionConfigStore's mutable field motionConfig is not
thread-safe and must be made safe: replace the plain var access with a
thread-safe mechanism (e.g., change motionConfig to an
AtomicReference<MotionConfig?> or mark it `@Volatile` and synchronize
writes/reads) so provideMotionConfig() reads a safe snapshot; update
MotionConfigStore and provideMotionConfig() accordingly, ensuring any
setters/update code (if present) use AtomicReference.set/get or synchronized
blocks to avoid data races.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt`:
- Around line 29-32: The Lucene query in AlbumArtRemoteDataSourceImpl is
interpolating unescaped trackName and artistName into the query string (the
addQueryParameter call building "recording:\"$trackName\" AND
artist:\"$artistName\""), which can break or corrupt searches; fix by
introducing and using a helper (e.g., escapeLuceneSpecialChars or
escapeForLuceneQuery) to escape Lucene special characters (\, ", + - && || ! ( )
{ } [ ] ^ ~ * ? : etc.) in trackName and artistName before interpolation, and
replace the raw variables with the escaped values when calling addQueryParameter
so the query is always well-formed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt`:
- Around line 50-60: The response is not closed on error paths in the HTTP call
handling inside the search and get methods of LrcLibApiServiceImpl (the block
that calls client.newCall(request).execute()); wrap the OkHttp Response in
response.use { ... } (or explicitly call response.close() on all return/error
paths) so the body is always closed even when response.isSuccessful is false,
and move the success/failure checks and body.string() extraction into that use
block to return Result accordingly.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt`:
- Line 79: The duration string uses integer division causing truncated seconds;
change the expression in LyricsActivity.kt that builds the "Duration" line
(currently using (end - start) / provideMotionConfig().fps) to perform
floating-point division by converting operands to Double (e.g., (end -
start).toDouble() / provideMotionConfig().fps.toDouble()) and format the result
(for example to 2 decimal places) so the displayed seconds include fractional
precision; reference variables end, start and the provideMotionConfig() call
when making the change.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`:
- Around line 65-90: Change the bare lifecycleScope.launch collection in
SearchActivity to a lifecycle-aware collector: inside the activity lifecycle
(e.g., onCreate/onStart) use lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { collect lyricsViewModel.uiState } }
so lyricsViewModel.uiState is only collected while the activity is visible;
update the collector that handles LyricsUiState.Error, .Loading, .Success,
.Initial to run inside that repeatOnLifecycle block and add the necessary import
for repeatOnLifecycle.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt`:
- Line 151: The code uses integer division and repeatedly calls
provideMotionConfig() per item; hoist the motion config lookup outside the
LazyColumn (call provideMotionConfig() once, e.g., val motionConfig =
provideMotionConfig() in SyncedLyricsSelector) and replace the integer division
expression line.frame / provideMotionConfig().fps with a floating-point division
such as line.frame.toFloat() / motionConfig.fps (and format the result to the
desired precision) so times like 15 frames at 30fps render as 0.5 sec instead of
0 sec.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt`:
- Around line 51-53: Make the three publicly visible vals into private
properties to hide implementation details: change okHttp, remote, and repository
in LyricsContainer (the vals named okHttp, remote, repository) to private val so
they are not exposed on the view's public API; ensure any internal usages within
the class still reference these renamed-private properties and adjust any
external references to use public methods instead.
- Around line 89-97: The code assumes songName contains " - " and directly
accesses songDetails[1], causing IndexOutOfBoundsException; update the logic in
LyricsContainer (the block using songName.split(" - "),
repository.getAlbumArtUrl(...), repository.getAlbumArtBitmap(...) and
setImageBitmap(...)) to safely handle cases where the delimiter is absent—either
check songName.contains(" - ") or split with a max of 2 parts and guard that the
resulting list has at least 2 elements before calling repository.getAlbumArtUrl,
and only proceed to getAlbumArtBitmap and setImageBitmap when both artist/title
parts are present.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt`:
- Around line 88-89: The getter for selectedSongName currently interpolates
selectedLyric.value?.trackName and ?.artistName producing "null - null" when
selectedLyric.value is null; update the selectedSongName getter to check
selectedLyric.value and return either an empty string or a placeholder (e.g.,
"") when null, otherwise format "${trackName} - ${artistName}" using the
non-null properties; locate the getter for selectedSongName and modify it to use
a null-check (or Elvis operator/let) against selectedLyric.value to avoid
emitting the literal "null - null".

In `@modules/motion-store/build.gradle`:
- Around line 1-4: The plugins block currently applies
alias(libs.plugins.android.library) and id 'maven-publish' but is missing the
Kotlin Android plugin; update the plugins block to include
alias(libs.plugins.kotlin.android) so Kotlin sources (e.g., DatabaseProvider.kt,
MotionProject.kt, ExampleUnitTest.kt) compile, ensuring the new alias is added
alongside alias(libs.plugins.android.library) and id 'maven-publish'.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/DatabaseProvider.kt`:
- Line 13: The database filename is misleadingly set to "firestore-offline.db"
even though this is a Nitrite DB; locate the
.filePath(context.getDatabasePath("firestore-offline.db")) call (in
DatabaseProvider.kt) and change the hardcoded filename to a clearer name such as
"motion-store.db" or "nitrite-motion-store.db" so it accurately reflects the DB
type; update any related tests or configuration that reference the old filename
to use the new name.
- Line 20: Replace the hardcoded credentials used when opening the Nitrite DB in
DatabaseProvider (the .openOrCreate("user", "password") call) by either calling
the no-credentials overload (openOrCreate()) or reading credentials from
configuration/environment (e.g., a Config/Environment variable injected into the
DatabaseProvider) and passing those values; update the DatabaseProvider
initialization to avoid inline "user"/"password" literals and validate presence
of configured credentials before using them.
- Around line 7-9: DatabaseProvider's private constructor prevents any
instantiation—add a companion object providing a factory/singleton accessor
(e.g., companion object with a `@Volatile` private INSTANCE and a
getInstance(context: Context) method) that initializes DatabaseProvider with
context.applicationContext in a thread-safe synchronized block (or use
double-checked locking) and returns the singleton; alternatively, if multiple
instances are intended, change the constructor visibility to public and add a
create(context: Context) factory function in the companion object to construct
new DatabaseProvider instances.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt`:
- Around line 19-28: The companion object's from function currently has no
return type or body and therefore does nothing; change its signature to return a
MotionProject and implement the body to construct and return a MotionProject
instance using the provided id, name, path, and by converting MotionConfig and
VideoAspectRatio into the stored fields expected by MotionProject (use existing
mappers/serializers or constructors for MotionConfig and VideoAspectRatio where
applicable). Specifically update companion object fun from(...) to return
MotionProject and populate all MotionProject properties from the passed
parameters (use methods on MotionConfig/VideoAspectRatio or existing conversion
helpers in the module to produce the stored representations).

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseContourMotionView.kt`:
- Line 8: Remove the unused import for MotionConfig from BaseContourMotionView
(the top import "import com.tejpratapsingh.motionlib.core.MotionConfig"); simply
delete that import line in BaseContourMotionView.kt so the file no longer
references MotionConfig, then rebuild to ensure no other references exist.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/container/RotatingMotionView.kt`:
- Line 7: Remove the unused import of MotionVideoProducer from
RotatingMotionView (the import statement "import
com.tejpratapsingh.motionlib.core.motion.MotionVideoProducer") since it is never
referenced in the file; simply delete that import to resolve the unused-import
warning and keep imports minimal.

In
`@modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/ConfigParser.kt`:
- Around line 17-22: The JsonObject.toMotionConfig() method currently calls
get("aspectRatio").asJsonObject and get("fps").asInt /
get("outputQuality").asInt without null-safety; update this function to
defensively handle missing or malformed fields by using safe access and defaults
or by catching parse errors: e.g., check get("fps") and get("outputQuality") for
null (or use the safe-call operator and elvis defaults) and validate
get("aspectRatio") before calling toVideoAspectRatio(), or wrap the whole
parsing in a try/catch and return a fallback MotionConfig on error; ensure you
reference JsonObject.toMotionConfig, MotionConfig, and the get(...).asInt /
get(...).asJsonObject usages when making the changes.
🧹 Nitpick comments (21)
modules/pytorch-motion-ext/build.gradle (1)

23-29: compileOptions targets Java 11 — confirm this is intentional given the JDK 21 project upgrade.

The project-wide JDK was bumped to 21, but this module still targets JavaVersion.VERSION_11 for source/target compatibility. This is valid (compile for a lower target), but worth confirming it's deliberate and not an oversight.

modules/motion-store/src/test/java/com/tejpratapsingh/motionstore/ExampleUnitTest.kt (1)

1-16: Boilerplate placeholder test — consider adding real unit tests for the motion-store module.

This is the same auto-generated test present in every module. Given that motion-store introduces a Nitrite database layer (per the PR summary), meaningful tests for DatabaseProvider and MotionProject would be valuable.

modules/lyrics-maker/src/main/res/layout/lyrics_container.xml (1)

70-84: pb_progress still references fake_chart_view via constraint, but both are effectively disconnected.

pb_progress is now gone (line 81) and constrains its bottom to fake_chart_view (line 82), which itself no longer anchors to btn_container. Since pb_progress is invisible, there's no runtime issue, but if these elements are later toggled visible (e.g., via SDUI), the constraint chain between pb_progress → fake_chart_view → parent may produce unexpected positioning. Worth a quick sanity check if visibility toggling is planned.

gradle.properties (1)

23-31: Consider removing android.enableJetifier=true (line 19) during this build modernization.

Jetifier has been deprecated since AGP 7.x and adds build overhead. Since you're upgrading to Gradle 9.1.0 and modernizing the build, this is a good opportunity to verify all transitive dependencies are AndroidX-native and drop it.

Also, the newly added flags appear to opt out of several Gradle 9 / AGP defaults (e.g., strictFullModeForKeepRules=false, optimizedResourceShrinking=false). These are reasonable for a safe migration, but consider revisiting them once the upgrade stabilizes to benefit from the improved defaults.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/di/OkHttpProvider.kt (1)

7-26: Consider adding a writeTimeout and whether fully disabling connection pooling is intentional.

You set connectTimeout and readTimeout but no writeTimeout — OkHttp defaults to 10s which is likely fine, but for consistency you may want to be explicit.

The ConnectionPool(0, ...) + Connection: close header fully disables connection reuse. This is fine for infrequent requests but will add latency (TCP+TLS handshake per request) if the client makes bursts of calls to the same host (e.g., MusicBrainz lookup followed by cover art fetch).

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt (1)

83-84: Cache provideMotionConfig() in a local variable to avoid redundant calls.

onMeasure is called frequently during layout passes. Calling provideMotionConfig() twice is unnecessary when a single local val suffices.

♻️ Suggested refactor
-            val desiredWidth = provideMotionConfig().aspectRatio.width
-            val desiredHeight = provideMotionConfig().aspectRatio.height
+            val config = provideMotionConfig()
+            val desiredWidth = config.aspectRatio.width
+            val desiredHeight = config.aspectRatio.height
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/background/GradientView.kt (1)

64-73: Same provider pattern — consider caching in a local val.

Minor readability improvement: capture provideMotionConfig() once instead of calling it twice.

♻️ Suggested refactor
+        val config = provideMotionConfig()
         contourWidthOf {
-            provideMotionConfig()
-                .aspectRatio.width
+            config.aspectRatio.width
                 .toXInt()
         }
         contourHeightOf {
-            provideMotionConfig()
-                .aspectRatio.height
+            config.aspectRatio.height
                 .toYInt()
         }
modules/3d-filament-renderer/src/main/java/com/tejpratapsingh/motionlib/filamentrenderer/Filament3dView.kt (1)

67-88: Six separate provideMotionConfig() calls in this class — extract to a local or field.

provideMotionConfig() is called in initializeFilament() (4 times) and setupCamera() (2 times). Caching the config once at the call site improves readability and avoids any theoretical inconsistency if the backing store were updated between calls.

♻️ Suggested refactor for initializeFilament
  private fun initializeFilament() {
      Utils.init()
+     val config = provideMotionConfig()
      surfaceTexture = SurfaceTexture(0)
      surfaceTexture.setDefaultBufferSize(
-         provideMotionConfig().aspectRatio.width,
-         provideMotionConfig().aspectRatio.height,
+         config.aspectRatio.width,
+         config.aspectRatio.height,
      )
      surface = Surface(surfaceTexture)
      engine = Engine.create()
      swapChain = engine.createSwapChain(surface, 0)
      scene = engine.createScene()
      view = engine.createView()
      view.scene = scene
      renderer = engine.createRenderer()
      view.viewport =
          Viewport(
              0,
              0,
-             provideMotionConfig().aspectRatio.width,
-             provideMotionConfig().aspectRatio.height,
+             config.aspectRatio.width,
+             config.aspectRatio.height,
          )
  }
modules/3d-opengl-renderer/src/main/java/com/tejpratapsingh/motionlib/openglrenderer/MotionOpenGlView.kt (1)

34-35: provideMotionConfig() called twice — risk of inconsistent width/height.

If the backing config changes between the two calls, width and height could come from different configs. Store the result in a local val and read both properties from it.

Proposed fix
+    val motionConfig = provideMotionConfig()
     val offscreenRenderer =
         Object3DToBitmapRenderer(
             context = context,
             assetFileName = modelAssetPath,
-            width = provideMotionConfig().aspectRatio.width,
-            height = provideMotionConfig().aspectRatio.height,
+            width = motionConfig.aspectRatio.width,
+            height = motionConfig.aspectRatio.height,
             objectColor = floatArrayOf(0.7f, 0.3f, 0.3f, 1.0f),
         )
modules/app/src/main/java/com/tejpratapsingh/animator/ui/view/RenaultCar.kt (1)

53-62: Same pattern: cache provideMotionConfig() in a local val.

Two calls in the init block — store the result once for consistency and clarity, same as noted in MotionOpenGlView.kt.

Proposed fix
+        val motionConfig = provideMotionConfig()
         contourHeightOf {
-            provideMotionConfig()
-                .aspectRatio.height
+            motionConfig
+                .aspectRatio.height
                 .toYInt()
         }
         contourWidthOf {
-            provideMotionConfig()
-                .aspectRatio.width
+            motionConfig
+                .aspectRatio.width
                 .toXInt()
         }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSource.kt (1)

5-13: Inverted dependency: data source depends on repository for CoverSize.

Typically the repository depends on the data source, not vice versa. Having AlbumArtRemoteDataSource reference AlbumArtRepository.CoverSize inverts this direction. Consider extracting CoverSize into its own top-level enum or a shared types file so neither layer depends on the other for type definitions.

modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/RenaultCar.kt (1)

75-84: Consider caching the config to a local val.

provideMotionConfig() is called twice in the init block. If the provider ever becomes non-trivial, this could return different instances between calls. A single val config = provideMotionConfig() would be more robust.

Proposed fix
+        val config = provideMotionConfig()
         contourHeightOf {
-            provideMotionConfig()
-                .aspectRatio.height
+            config.aspectRatio.height
                 .toYInt()
         }
         contourWidthOf {
-            provideMotionConfig()
-                .aspectRatio.width
+            config.aspectRatio.width
                 .toXInt()
         }
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt (2)

46-63: New Gson/GsonBuilder instances allocated on every call.

Both toJsonObject() and toVideoAspectRatio() create new Gson instances per invocation. If these are called frequently (e.g., in a list or loop), consider extracting the configured Gson to a module-level val.


26-44: Consider adding a comment or test to prevent allRatios from drifting.

Currently, the allRatios list correctly includes all 16 non-Custom sealed subtypes of VideoAspectRatio. However, if a new subtype is added to the sealed class without updating this list, deserialization will silently fall back to Custom. A comment above allRatios or a test validating completeness would prevent future maintenance issues.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt (1)

45-54: Gson instances are created on every toJson()/fromJson() call.

Consider hoisting a shared Gson instance into the companion object to avoid repeated construction.

♻️ Proposed fix
     companion object {
+        private val gson = Gson()
+
         fun fromJson(json: String): LyricsResponse {
-            val gson = Gson()
             return gson.fromJson(json, LyricsResponse::class.java)
         }
     }
+
+    fun toJson(): String = gson.toJson(this)
+
+    companion object {
+        // move gson here
+    }
modules/motion-store/src/androidTest/java/com/tejpratapsingh/motionstore/ExampleInstrumentedTest.kt (1)

1-22: Boilerplate test — consider adding actual tests for DatabaseProvider.

This is the default Android Studio template. Given that DatabaseProvider handles database initialization with credentials and file paths, it would benefit from dedicated tests verifying open/close lifecycle and data persistence.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LyricsRepositoryImpl.kt (1)

10-15: Pure pass-through to apiService — consider if this layer adds value now.

Currently this repository is a 1:1 delegation with no caching, mapping, or error enrichment. It's fine as a structural placeholder, but if no additional logic is planned (e.g., caching, offline support), this could be simplified.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt (1)

24-69: Significant code duplication between search and get.

Both methods share the same pattern: build URL → construct request → execute → check success → read body → parse JSON. Consider extracting a private helper to reduce duplication.

Also applies to: 71-115

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt (1)

96-144: Extract local variable to avoid repeated state.lyrics[item] indexing.

state.lyrics[item] is accessed five times in the loop body. A local val lyric = state.lyrics[item] at the top of the lambda improves readability and avoids repeated list indexing.

Suggested refactor
                 items(state.lyrics.size) { item ->
+                    val lyric = state.lyrics[item]
                     Card(
                         modifier =
                             Modifier
                                 .fillMaxWidth()
                                 .padding(vertical = 4.dp)
-                                .clickable { onLyricsSelected(state.lyrics[item]) },
+                                .clickable { onLyricsSelected(lyric) },
                         // ...
                     ) {
                         Text(
-                            text = "${state.lyrics[item].trackName} - ${state.lyrics[item].artistName}",
+                            text = "${lyric.trackName} - ${lyric.artistName}",
                             // ...
                         )
                         Text(
-                            text = "Duration: ${state.lyrics[item].getReadableDuration()}",
+                            text = "Duration: ${lyric.getReadableDuration()}",
                             // ...
                         )
                         Text(
-                            text = state.lyrics[item].getLyrics(),
+                            text = lyric.getLyrics(),
                             // ...
                         )
                     }
                 }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (2)

57-86: Concurrent searches can race — later query may be overwritten by an earlier response.

If the user triggers searchLyrics rapidly (e.g., clicking multiple recent searches), each call launches independently in viewModelScope. A slow first request completing after a fast second request would overwrite the UI state with stale results.

Cancel the previous job before launching a new one:

Proposed fix
 class LyricsViewModel : ViewModel() {
     // ... existing fields ...
+    private var searchJob: Job? = null

     fun searchLyrics(
         trackName: String? = null,
         artistName: String? = null,
         albumName: String? = null,
         query: String? = null,
     ) {
-        viewModelScope.launch {
+        searchJob?.cancel()
+        searchJob = viewModelScope.launch {
             _uiState.value = LyricsUiState.Loading
             // ...
         }
     }

42-46: Manual wiring of dependencies inside the ViewModel.

Constructing OkHttpClient, Gson, LrcLibApiServiceImpl, and LyricsRepositoryImpl directly in the ViewModel couples it to concrete implementations and makes testing harder. Consider injecting LyricsRepository via the constructor (e.g., using a ViewModelProvider.Factory or Hilt) so the ViewModel stays testable and decoupled.

<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

IDE bytecode target (21) is inconsistent with module-level compileOptions (Java 11).

All module build.gradle files in this PR still set sourceCompatibility/targetCompatibility to JavaVersion.VERSION_11. Gradle controls the actual build, so this IDE setting won't affect artifacts, but the mismatch can cause confusing IDE-vs-CLI compilation differences. Consider aligning these — either bump compileOptions to Java 21 across modules, or keep this at 11 to match.

🤖 Prompt for AI Agents
In @.idea/compiler.xml at line 4, The IDE bytecodeTargetLevel is set to "21" but
module compileOptions/sourceCompatibility and targetCompatibility are still
JavaVersion.VERSION_11; update one side so they match. Either change the
<bytecodeTargetLevel target="21"> entry to target="11" to match existing
compileOptions/sourceCompatibility and targetCompatibility, or update all module
build.gradle settings (sourceCompatibility/targetCompatibility and any
compileOptions.release) to Java 21 so Gradle and the IDE are consistent; adjust
the corresponding symbol(s) bytecodeTargetLevel and the Gradle properties
sourceCompatibility/targetCompatibility/compileOptions.release accordingly.

Copy link

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

79-100: ⚠️ Potential issue | 🔴 Critical

runBlocking in a View's init block will freeze the UI thread (ANR risk).

runBlocking on the main thread performs synchronous network I/O (MusicBrainz lookup + image download) during view construction. On Android, this will block the UI thread and likely trigger an Application Not Responding (ANR) dialog. Use a coroutine scope (e.g., lifecycleScope or a passed-in CoroutineScope) with launch instead, and set the image asynchronously with a placeholder or visibility toggle.

modules/3d-opengl-renderer/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore the kotlin.android plugin — this module contains Kotlin source files and will fail to compile without it.

The kotlin.android plugin was removed but the module has four Kotlin source files (MotionOpenGlView.kt, ObjModel.kt, Object3DToBitmapRenderer.kt, OffscreenRenderer.kt) that require the plugin to be compiled. Removing it while retaining libs.androidx.core.ktx as a dependency will cause a build failure.

modules/jcodec-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

The Kotlin Android plugin must not be removed—the module contains Kotlin source files.

The kotlin.android plugin alias was removed, but this module has three Kotlin source files that require it to compile:

  • modules/jcodec-motion-ext/src/main/java/com/tejpratapsingh/motionlib/jcodec/FileExtension.kt
  • modules/jcodec-motion-ext/src/main/java/com/tejpratapsingh/motionlib/jcodec/JCodecVideoProducerAdapter.kt
  • modules/jcodec-motion-ext/src/test/java/com/tejpratapsingh/motionlib/jcodec/ExampleUnitTest.kt

Without the Kotlin plugin, the build will fail. Restore the alias(libs.plugins.kotlin.android) line.

modules/motionlib/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Add Kotlin plugin back to motionlib/build.gradle—this module has 17+ Kotlin source files and will fail to compile without it.

The kotlin-android plugin was removed from this module, but it contains Kotlin sources (MotionComposerView.kt, BaseFrameMotionView.kt, BaseContourMotionView.kt, MotionVideoProducer.kt, and many others). The Kotlin plugin is not applied globally (neither in settings.gradle nor in the root build.gradle), so without it, Gradle cannot compile the .kt files and the build will fail.

Restore alias(libs.plugins.kotlin.android) in the plugins block.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt (1)

69-70: ⚠️ Potential issue | 🟠 Major

minBy/maxBy on an empty lyrics list will throw NoSuchElementException.

The lyrics property falls back to emptyList() when the intent extra is missing. If that happens, minBy and maxBy crash. Add an early return or guard.

Proposed fix
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+        if (lyrics.isEmpty()) {
+            finish()
+            return
+        }
+
         val start = lyrics.minBy { it.frame }.frame
         val end = lyrics.maxBy { it.frame }.frame
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

103-113: ⚠️ Potential issue | 🔴 Critical

field.first() will throw NoSuchElementException on an empty list.

The backing field defaults to emptyList() (line 103). Any access to selectedLyrics before it's reassigned to a non-empty list will crash. Use firstOrNull() with an early return, or guard against the empty case.

Proposed fix
     var selectedLyrics: List<SyncedLyricFrame> = emptyList()
         get() {
+            if (field.isEmpty()) return emptyList()
             val firstFrame = field.first().frame
             return field
                 .map {
🤖 Fix all issues with AI agents
In `@gradle/libs.versions.toml`:
- Around line 21-22: The Kotlin version in libs.versions.toml is pinned to
kotlinVersion = "2.3.0", which is not officially compatible with agpVersion =
"9.0.0"; update kotlinVersion to a compatible release (e.g., "2.3.10" or later)
so Kotlin officially supports AGP 9.0.0, ensuring the kotlinVersion entry is
changed accordingly and any build files referencing kotlinVersion pick up the
new value.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt`:
- Around line 63-73: The fetchBitmap implementation can throw IOException when
calling client.newCall(request).execute(), so wrap the network call inside a
try/catch in the withContext(Dispatchers.IO) block (around
client.newCall(request).execute().use { ... }) to catch IOException (and
optionally other exceptions) and return null on error; keep the existing null
returns for non-successful responses and decoding failures, and optionally log
the exception for debugging in the catch block.
- Around line 44-60: Wrap the network call and JSON parsing inside a try-catch
in the withContext block so IO and serialization exceptions are caught and the
method returns null instead of throwing; specifically surround
client.newCall(request).execute() and
json.decodeFromString<MusicBrainzResponse>(bodyString) with a try { ... } catch
(e: Exception) { /* optionally log e */ return@withContext null }, keeping the
existing success path that builds the cover art URL (the code in
AlbumArtRemoteDataSourceImpl that reads response.body()?.string(), parses into
MusicBrainzResponse, extracts releaseId and returns the URL should remain inside
the try block).

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt`:
- Line 38: Remove the unused import of MotionVideoProducer from
SyncedLyricsSelector.kt; locate the import statement "import
com.tejpratapsingh.motionlib.core.motion.MotionVideoProducer" at the top of the
file and delete it so the file no longer contains an unused reference to
MotionVideoProducer.

In
`@modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt`:
- Around line 53-60: The deserializer currently calls obj.get("width").asInt /
obj.get("height").asInt / obj.get("label").asString without guarding for missing
keys which causes NPEs; update the JsonDeserializer block to defensively parse
these fields: use obj.has("width")/obj.has("height")/obj.has("label") (or check
obj.get(...) != null) before calling asInt/asString, throw a JsonParseException
or provide sensible defaults if a required field is absent, and then continue to
lookup in allRatios or construct Custom(width, height, label) — reference the
JsonDeserializer lambda, the element variable, allRatios.find { it.width ==
width && it.height == height }, and the Custom(width, height, label) constructor
when making the change.
🧹 Nitpick comments (14)
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

82-86: Inconsistent HTTP clients: Ktor HttpClient(CIO) here vs. OkHttp via repository in the else branch.

The image != null path creates a one-off Ktor HttpClient for fetching the bitmap, while the else branch uses the OkHttp-backed repository. Consider using repository.getAlbumArtBitmap(image) (or the OkHttp-based remote) for consistency and to avoid pulling in two HTTP client engines.

♻️ Proposed fix
                 if (image != null) {
-                    val client = HttpClient(CIO)
                     Log.i(TAG, "Using image from social meta: $image")
-                    setImageBitmap(client.fetchBitmap(image))
-                    client.close()
-                    return@runBlocking
+                    repository.getAlbumArtBitmap(image)?.also {
+                        setImageBitmap(it)
+                    }
                 } else {
gradle/wrapper/gradle-wrapper.properties (1)

3-3: Consider upgrading to Gradle 9.3.1 (latest stable) for improved performance and stability.

Gradle 9.1.0 is valid, but 9.3.1 is the latest stable release available (released January 29, 2026). Gradle 9.2.0+ introduced several performance and stability improvements over 9.1.0.

Since this is a major version jump (8.13 → 9.1.0), be aware that Gradle 9.0.0+ requires Java 17+ to run and uses Kotlin 2.2.0 and Groovy 4, which can introduce breaking changes. However, your codebase is already configured with AGP 9.0.0 and Kotlin 2.3.0, which are compatible with Gradle 9.x, so no plugin compatibility issues are expected.

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt (1)

83-84: Cache provideMotionConfig() to avoid redundant calls in onMeasure.

onMeasure is called frequently during layout passes. Calling provideMotionConfig() twice risks both unnecessary overhead and a (theoretical) inconsistency if the backing config changes between calls.

♻️ Proposed fix
         override fun onMeasure(
             widthMeasureSpec: Int,
             heightMeasureSpec: Int,
         ) {
-            val desiredWidth = provideMotionConfig().aspectRatio.width
-            val desiredHeight = provideMotionConfig().aspectRatio.height
+            val config = provideMotionConfig()
+            val desiredWidth = config.aspectRatio.width
+            val desiredHeight = config.aspectRatio.height
             setMeasuredDimension(desiredWidth, desiredHeight)
modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/RenaultCar.kt (1)

12-13: MotionVideoProducer import appears unused.

Line 12 imports MotionVideoProducer, but it is not referenced anywhere in this file. This may be a leftover from a refactoring step.

🧹 Remove unused import
-import com.tejpratapsingh.motionlib.core.motion.MotionVideoProducer
 import com.tejpratapsingh.motionlib.core.provideMotionConfig
modules/3d-opengl-renderer/src/main/java/com/tejpratapsingh/motionlib/openglrenderer/MotionOpenGlView.kt (2)

30-37: Call provideMotionConfig() once to avoid inconsistency and redundant lookups.

If the global config were ever swapped between the two calls, width and height could come from different configs. Store the result in a local:

♻️ Proposed fix
+    private val motionConfig = provideMotionConfig()
+
     val offscreenRenderer =
         Object3DToBitmapRenderer(
             context = context,
             assetFileName = modelAssetPath,
-            width = provideMotionConfig().aspectRatio.width,
-            height = provideMotionConfig().aspectRatio.height,
+            width = motionConfig.aspectRatio.width,
+            height = motionConfig.aspectRatio.height,
             objectColor = floatArrayOf(0.7f, 0.3f, 0.3f, 1.0f),
         )

10-10: Unused import: MotionVideoProducer is not referenced in this file.

This import appears to be leftover from another change. Consider removing it to keep the file clean.

modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/DatabaseProvider.kt (1)

16-21: db is publicly exposed as a mutable Nitrite instance — breaks encapsulation.

The val db property is public, allowing any consumer to directly manipulate the database (drop collections, close it, etc.) bypassing DatabaseProvider's own close() method. Consider making it private or internal and exposing only specific repository/collection accessors.

♻️ Proposed fix
-    val db =
+    private val db =
         Nitrite
             .builder()
             .loadModule(storeModule)
-            .openOrCreate("user", "password")
+            .openOrCreate()
modules/motionlib/build.gradle (1)

22-25: VERSION_1_8 is inconsistent with other modules using VERSION_11.

Other modules in this PR (e.g., tensorflow-motion-ext, 3d-filament-renderer) use JavaVersion.VERSION_11. This module still targets Java 1.8. While this isn't a new change, it's worth aligning for consistency, especially if downstream modules depend on Java 11 features.

modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt (2)

46-46: Gson instances are created on every call — consider reusing a shared instance.

Both toJsonObject() and toVideoAspectRatio() allocate new Gson/GsonBuilder instances per invocation. For a utility that may be called repeatedly (e.g., parsing a list of configs), this adds unnecessary GC pressure.

♻️ Proposed fix — hoist Gson instances to file-level
+private val gson = Gson()
+
+private val videoAspectRatioGson: Gson =
+    GsonBuilder()
+        .registerTypeAdapter(
+            VideoAspectRatio::class.java,
+            JsonDeserializer { element, _, _ ->
+                val obj = element.asJsonObject
+                val width = obj.get("width")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'width'")
+                val height = obj.get("height")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'height'")
+                val label = obj.get("label")?.asString ?: "${width}x${height}"
+                allRatios.find { it.width == width && it.height == height }
+                    ?: Custom(width, height, label)
+            },
+        ).create()
+
-fun VideoAspectRatio.toJsonObject(): JsonObject = Gson().toJsonTree(this).asJsonObject
+fun VideoAspectRatio.toJsonObject(): JsonObject = gson.toJsonTree(this).asJsonObject
 
-fun JsonObject.toVideoAspectRatio(): VideoAspectRatio {
-    val gson =
-        GsonBuilder()
-            ...
-    return gson.fromJson(this, VideoAspectRatio::class.java)
-}
+fun JsonObject.toVideoAspectRatio(): VideoAspectRatio =
+    videoAspectRatioGson.fromJson(this, VideoAspectRatio::class.java)

Also applies to: 48-63


26-44: Consider automating ratio discovery to prevent maintenance drift.

VideoAspectRatio is a sealed class, and all 16 predefined ratios are currently in allRatios. However, if a new ratio is added to VideoAspectRatio in the future but forgotten in allRatios, deserialization silently falls back to Custom. Since Kotlin sealed classes support runtime introspection, consider using VideoAspectRatio::class.sealedSubclasses or reflection to auto-discover predefined ratios instead of maintaining this list manually.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt (1)

7-7: Unused import: kotlinx.serialization.Serializable.

The @Serializable annotation was removed from the class but its import remains. This is dead code.

Proposed fix
-import kotlinx.serialization.Serializable
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt (1)

59-63: Extract duplicated fallback expression.

it.title ?: it.description ?: "" is repeated on lines 61 and 62. Extract it to a local variable for clarity and to avoid divergence if the fallback logic changes.

Proposed fix
         ShareReceiverActivity.readMetadataFromIntent(intent)?.let {
             lyricsViewModel.socialMeta.value = it
-            lyricsViewModel.query.value = it.title ?: it.description ?: ""
-            lyricsViewModel.searchLyrics(it.title ?: it.description ?: "")
+            val searchQuery = it.title ?: it.description ?: ""
+            lyricsViewModel.query.value = searchQuery
+            lyricsViewModel.searchLyrics(query = searchQuery)
         }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (2)

50-52: Consider exposing query, socialMeta, and selectedLyric as read-only StateFlow publicly.

Exposing MutableStateFlow directly allows the View layer to mutate ViewModel state without going through defined functions, which undermines unidirectional data flow. Consider backing these with private MutableStateFlow and exposing public StateFlow (similar to uiState), with dedicated update methods.


42-46: Manual service/repository wiring in the ViewModel constructor body.

Instantiating OkHttpProvider.httpClient, Gson(), LrcLibApiServiceImpl, and LyricsRepositoryImpl directly couples the ViewModel to concrete implementations, making it harder to test. This is fine for now, but consider constructor injection (e.g., via a ViewModelFactory or Hilt) as the module grows.

Comment on lines +63 to +73
override suspend fun fetchBitmap(url: String): Bitmap? =
withContext(Dispatchers.IO) {
val request = Request.Builder().url(url).build()

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null

val bytes = response.body()?.bytes() ?: return@withContext null
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same missing exception handling in fetchBitmap.

execute() can throw IOException here as well, crashing instead of returning null.

🐛 Proposed fix
     override suspend fun fetchBitmap(url: String): Bitmap? =
         withContext(Dispatchers.IO) {
-            val request = Request.Builder().url(url).build()
-
-            client.newCall(request).execute().use { response ->
-                if (!response.isSuccessful) return@withContext null
-
-                val bytes = response.body()?.bytes() ?: return@withContext null
-                BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
-            }
+            try {
+                val request = Request.Builder().url(url).build()
+                client.newCall(request).execute().use { response ->
+                    if (!response.isSuccessful) return@withContext null
+                    val bytes = response.body()?.bytes() ?: return@withContext null
+                    BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+                }
+            } catch (e: Exception) {
+                Log.e("AlbumArtRemoteDataSource", "fetchBitmap failed", e)
+                null
+            }
         }
📝 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 suspend fun fetchBitmap(url: String): Bitmap? =
withContext(Dispatchers.IO) {
val request = Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null
val bytes = response.body()?.bytes() ?: return@withContext null
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
}
override suspend fun fetchBitmap(url: String): Bitmap? =
withContext(Dispatchers.IO) {
try {
val request = Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@withContext null
val bytes = response.body()?.bytes() ?: return@withContext null
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
} catch (e: Exception) {
Log.e("AlbumArtRemoteDataSource", "fetchBitmap failed", e)
null
}
}
🤖 Prompt for AI Agents
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt`
around lines 63 - 73, The fetchBitmap implementation can throw IOException when
calling client.newCall(request).execute(), so wrap the network call inside a
try/catch in the withContext(Dispatchers.IO) block (around
client.newCall(request).execute().use { ... }) to catch IOException (and
optionally other exceptions) and return null on error; keep the existing null
returns for non-successful responses and decoding failures, and optionally log
the exception for debugging in the catch block.

Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt (1)

69-70: ⚠️ Potential issue | 🟠 Major

minBy/maxBy will throw NoSuchElementException if lyrics is empty.

lyrics can be an empty list (the getter defaults to emptyList()). If the activity is started without valid lyrics data, these calls crash immediately in onCreate.

Consider guarding with an early return or using minByOrNull/maxByOrNull.

🐛 Proposed fix
-        val start = lyrics.minBy { it.frame }.frame
-        val end = lyrics.maxBy { it.frame }.frame
+        if (lyrics.isEmpty()) {
+            finish()
+            return
+        }
+        val start = lyrics.minOf { it.frame }
+        val end = lyrics.maxOf { it.frame }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

79-100: ⚠️ Potential issue | 🟠 Major

runBlocking in init performs network I/O on the calling thread, likely the main/UI thread.

This will freeze the UI during album art fetching and can easily trigger an ANR. While the runBlocking wrapper pre-dates this PR, the newly introduced repository.getAlbumArtUrl and repository.getAlbumArtBitmap calls are suspend functions performing network I/O that will block here. Consider moving this to a coroutine scope (e.g., lifecycleScope or a CoroutineScope passed into the view) so the network calls happen off the main thread.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

103-113: ⚠️ Potential issue | 🔴 Critical

selectedLyrics getter crashes with NoSuchElementException when the backing list is empty.

field defaults to emptyList(), and the getter unconditionally calls field.first() on Line 105. Any access before a non-empty list is assigned (or after assigning an empty list) will crash at runtime.

Proposed fix
     var selectedLyrics: List<SyncedLyricFrame> = emptyList()
         get() {
+            if (field.isEmpty()) return emptyList()
             val firstFrame = field.first().frame
             return field
                 .map {
                     SyncedLyricFrame(
                         frame = it.frame - firstFrame,
                         text = it.text,
                     )
                 }.sortedBy { it.frame }
         }
🤖 Fix all issues with AI agents
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt`:
- Line 7: Remove the unused import kotlinx.serialization.Serializable from
LyricsResponse.kt; locate the import statement for
kotlinx.serialization.Serializable (the leftover after removing the
`@Serializable` annotation from the LyricsResponse class) and delete that import
so there are no unused imports in the file.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt`:
- Around line 148-179: The current when-block only handles LyricsUiState.Success
and uses the else branch to render recentSearches, which swallows
LyricsUiState.Error; add an explicit branch for LyricsUiState.Error (matching
the sealed/state class used) that displays the error message from the error
state (e.g., state.message) and a retry affordance that calls
viewModel.searchLyrics(...) or re-emits the query (using viewModel.query.tryEmit
or the existing clickable handler), while keeping the recentSearches rendering
for the Initial/Loading branches or adding separate Loading UI; update the when
to handle LyricsUiState.Error, LyricsUiState.Loading and LyricsUiState.Initial
explicitly instead of using the catch-all else so users see errors and can
retry.
🧹 Nitpick comments (10)
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt (1)

59-63: Extract duplicated fallback expression into a local variable.

it.title ?: it.description ?: "" is repeated on lines 61 and 62. A local val avoids the duplication and makes future changes less error-prone.

♻️ Suggested refactor
         ShareReceiverActivity.readMetadataFromIntent(intent)?.let {
             lyricsViewModel.socialMeta.value = it
-            lyricsViewModel.query.value = it.title ?: it.description ?: ""
-            lyricsViewModel.searchLyrics(it.title ?: it.description ?: "")
+            val searchQuery = it.title ?: it.description ?: ""
+            lyricsViewModel.query.value = searchQuery
+            lyricsViewModel.searchLyrics(searchQuery)
         }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt (1)

96-142: Extract item to a local variable and provide a stable key to items.

state.lyrics[item] is dereferenced five times inside the lambda. Pulling it into a local val improves readability and avoids redundant list lookups. Additionally, LyricsResponse has a stable id field that should be used as the key for the items composable, allowing Compose to correctly diff the list when contents change and prevent stale recompositions.

♻️ Proposed refactor
-                    items(state.lyrics.size) { item ->
+                    items(state.lyrics.size, key = { state.lyrics[it].id }) { item ->
+                        val lyrics = state.lyrics[item]
                         Card(
                             modifier =
                                 Modifier
                                     .fillMaxWidth()
                                     .padding(vertical = 4.dp)
-                                    .clickable { onLyricsSelected(state.lyrics[item]) },
+                                    .clickable { onLyricsSelected(lyrics) },
                             colors =
                                 CardDefaults.cardColors(
                                     containerColor = MaterialTheme.colorScheme.surfaceVariant,
                                 ),
                         ) {
                             Text(
-                                text = "${state.lyrics[item].trackName} - ${state.lyrics[item].artistName}",
+                                text = "${lyrics.trackName} - ${lyrics.artistName}",
                                 style = MaterialTheme.typography.labelLarge,
                                 modifier =
                                     Modifier.padding(
                                         start = 16.dp,
                                         top = 16.dp,
                                         end = 16.dp,
                                         bottom = 2.dp,
                                     ),
                             )
                             Text(
-                                text = "Duration: ${state.lyrics[item].getReadableDuration()}",
+                                text = "Duration: ${lyrics.getReadableDuration()}",
                                 maxLines = 2,
                                 style = MaterialTheme.typography.bodyMedium,
                                 modifier =
                                     Modifier.padding(
                                         start = 16.dp,
                                         top = 2.dp,
                                         end = 16.dp,
                                         bottom = 2.dp,
                                     ),
                             )
                             Text(
-                                text = state.lyrics[item].getLyrics(),
+                                text = lyrics.getLyrics(),
                                 maxLines = 2,
                                 style = MaterialTheme.typography.bodyMedium,
                                 modifier =
                                     Modifier.padding(
                                         start = 16.dp,
                                         top = 2.dp,
                                         end = 16.dp,
                                         bottom = 16.dp,
                                     ),
                             )
                             Spacer(Modifier.height(4.dp))
                         }
modules/3d-opengl-renderer/src/main/java/com/tejpratapsingh/motionlib/openglrenderer/MotionOpenGlView.kt (1)

34-35: Consider caching the config in a local variable to avoid redundant lookups.

provideMotionConfig() is called twice in succession. While likely cheap, a single val config = provideMotionConfig() would be clearer and avoid any risk of the two calls returning different instances if the store is mutated concurrently.

♻️ Suggested change
+    val motionConfig = provideMotionConfig()
     val offscreenRenderer =
         Object3DToBitmapRenderer(
             context = context,
             assetFileName = modelAssetPath,
-            width = provideMotionConfig().aspectRatio.width,
-            height = provideMotionConfig().aspectRatio.height,
+            width = motionConfig.aspectRatio.width,
+            height = motionConfig.aspectRatio.height,
             objectColor = floatArrayOf(0.7f, 0.3f, 0.3f, 1.0f),
         )
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/ConfigParser.kt (1)

7-15: Serialization creates a new Gson() instance on every call.

Consider extracting a shared Gson instance (e.g., a companion object or top-level val) to avoid allocating a new serializer per invocation. The same applies to VideoAspectRatioParser.kt.

modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt (2)

48-63: A new GsonBuilder + custom deserializer is allocated on every call.

toVideoAspectRatio() builds a fully configured Gson instance each invocation. Extract the Gson instance to a top-level or companion val so it's created once.

♻️ Suggested refactor
+private val videoAspectRatioGson: Gson =
+    GsonBuilder()
+        .registerTypeAdapter(
+            VideoAspectRatio::class.java,
+            JsonDeserializer { element, _, _ ->
+                val obj = element.asJsonObject
+                val width = obj.get("width")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'width'")
+                val height = obj.get("height")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'height'")
+                val label = obj.get("label")?.asString ?: "${width}x${height}"
+                allRatios.find { it.width == width && it.height == height }
+                    ?: Custom(width, height, label)
+            },
+        ).create()
+
 fun JsonObject.toVideoAspectRatio(): VideoAspectRatio {
-    val gson =
-        GsonBuilder()
-            .registerTypeAdapter(
-                VideoAspectRatio::class.java,
-                JsonDeserializer { element, _, _ ->
-                    val obj = element.asJsonObject
-                    val width = obj.get("width").asInt
-                    val height = obj.get("height").asInt
-                    val label = obj.get("label").asString
-
-                    allRatios.find { it.width == width && it.height == height }
-                        ?: Custom(width, height, label)
-                },
-            ).create()
-    return gson.fromJson(this, VideoAspectRatio::class.java)
+    return videoAspectRatioGson.fromJson(this, VideoAspectRatio::class.java)
 }

This also addresses the null-safety issue inline.


26-44: Use sealed-class reflection to auto-discover predefined ratios instead of manual list maintenance.

Currently allRatios is manually kept in sync with VideoAspectRatio subclasses. Since VideoAspectRatio is a sealed class, you can use sealedSubclasses to auto-discover all predefined ratio objects and filter out only the data objects. This eliminates the risk of silently falling back to Custom when a new ratio is added to the sealed class but not appended to allRatios.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (2)

50-52: Consider exposing query, socialMeta, and selectedLyric as StateFlow with private mutable backing fields.

Exposing MutableStateFlow directly allows any consumer to modify state, bypassing the ViewModel's control. The standard pattern uses a private _field / public field pair (as you already do for _uiState / uiState).


43-46: Consider injecting dependencies instead of creating them inline.

Instantiating OkHttpProvider.httpClient, Gson, LrcLibApiServiceImpl, and LyricsRepositoryImpl directly in the ViewModel makes it difficult to unit test (can't substitute mocks). A ViewModelFactory or a DI framework (Hilt/Koin) would improve testability.

gradle.properties (2)

23-31: Several AGP 9.0 defaults are explicitly disabled — document the rationale.

Properties like android.r8.strictFullModeForKeepRules=false, android.uniquePackageNames=false, and android.nonFinalResIds=false opt out of AGP 9.0's new defaults. This is a valid incremental migration strategy, but these should be tracked as tech debt to enable over time, as they represent best-practice improvements (e.g., strict R8 keep rules, unique package names).

Consider adding inline comments explaining why each is disabled.


19-19: Remove android.enableJetifier=true to improve build performance.

The codebase contains no old android.support.* libraries—all dependencies are already fully migrated to AndroidX. Jetifier is unnecessary and disabling it eliminates runtime overhead on every build. While still supported in AGP 9.0, Google recommends removing it when not needed.

import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused import: kotlinx.serialization.Serializable

The @Serializable annotation was removed from the class, but the import remains on line 7.

Proposed fix
-import kotlinx.serialization.Serializable
📝 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
import kotlinx.serialization.Serializable
🤖 Prompt for AI Agents
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt`
at line 7, Remove the unused import kotlinx.serialization.Serializable from
LyricsResponse.kt; locate the import statement for
kotlinx.serialization.Serializable (the leftover after removing the
`@Serializable` annotation from the LyricsResponse class) and delete that import
so there are no unused imports in the file.

Copy link

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
modules/lyrics-maker/build.gradle (1)

1-6: ⚠️ Potential issue | 🔴 Critical

Missing kotlin.android plugin — this module will not build.

This application module uses kotlin.serialization, kotlin.parcelize, and kotlin.compose — all of which require the Kotlin Android plugin as a prerequisite. Without it, Kotlin sources won't compile and these plugins will fail to apply correctly.

Proposed fix: restore the Kotlin Android plugin
 plugins {
     alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
     alias(libs.plugins.kotlin.serialization)
     alias(libs.plugins.kotlin.parcelize)
     alias(libs.plugins.kotlin.compose)
 }
modules/tensorflow-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore kotlin.android plugin — module contains Kotlin source files.

The module has 7 Kotlin files (ImageUtils.kt, TensorFlowImageProcessor.kt, CarBgRemover.kt, TfLiteSegmentationHelper.kt, TiledBackgroundRemover.kt, SuperResolutionProcessor.kt, and ExampleUnitTest.kt). Without the kotlin.android plugin, Gradle will fail to compile these sources.

modules/ffmpeg-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Kotlin source files exist but kotlin.android plugin was removed.

The module contains 4 Kotlin files (FfmpegVideoProducerAdapter.kt, FFMpegExtensions.kt, FFMpegVideoFrameView.kt, ExampleUnitTest.kt) but the kotlin.android plugin has been removed from the build configuration. Either restore the kotlin.android plugin or migrate the Kotlin files to Java.

modules/jcodec-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore the kotlin.android plugin—this module contains Kotlin source files.

The module has 3 Kotlin files (FileExtension.kt, JCodecVideoProducerAdapter.kt, and ExampleUnitTest.kt) that will fail to compile without the kotlin-android plugin. Removing it while retaining Kotlin sources will break the build.

modules/3d-filament-renderer/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Add kotlin.android plugin — Kotlin source files exist in this module.

The module contains 3 Kotlin source files (Filament3dView.kt, FilamentOffscreenCapturer.kt, ExampleUnitTest.kt) but the kotlin.android plugin is missing. This will cause compilation failures.

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/infra/AndroidVideoGenerator.kt (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Stale @param motionConfig in KDoc — parameter was removed.

The motionConfig parameter no longer exists in the method signature (it's now fetched internally via provideCurrentConfig()), but the KDoc still documents it. Remove or update this entry.

📝 Proposed fix
-     * `@param` motionConfig A [MotionConfig] object specifying video properties like
-     *                     width, height, and frames per second (fps).
modules/ffmpeg-motion-ext/src/main/java/com/tejpratapsingh/motionlib/ffmpeg/FfmpegVideoProducerAdapter.kt (1)

148-151: ⚠️ Potential issue | 🟠 Major

Integer division truncates audio trim/delay values.

track.startFrame / fps, track.endFrame / fps, and track.delayFrame / fps are all integer divisions, silently truncating fractional seconds. For example, frame 15 at 30 fps yields 0 instead of 0.5. The frameToSeconds helper on line 202 already handles this correctly with toDouble() but is unused here.

Proposed fix
-                val startSec = track.startFrame / fps
-                val endSec = track.endFrame / fps
-                val delayMs = (track.delayFrame / fps * 1000)
+                val startSec = track.startFrame.toDouble() / fps
+                val endSec = track.endFrame.toDouble() / fps
+                val delayMs = (track.delayFrame.toDouble() / fps * 1000).toLong()
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

102-112: ⚠️ Potential issue | 🔴 Critical

field.first() crashes with NoSuchElementException when selectedLyrics is empty.

The backing field is initialized to emptyList(). Any read of selectedLyrics before it is populated (or if set to an empty list) will throw. This is reachable from AppNavHost.onFinalizeLyricsActivity.start(... lyrics = viewModel.selectedLyrics ...).

🐛 Proposed fix — guard against empty list
     var selectedLyrics: List<SyncedLyricFrame> = emptyList()
         get() {
+            if (field.isEmpty()) return emptyList()
             val firstFrame = field.first().frame
             return field
                 .map {
                     SyncedLyricFrame(
                         frame = it.frame - firstFrame,
                         text = it.text,
                     )
                 }.sortedBy { it.frame }
         }
🤖 Fix all issues with AI agents
In `@gradle/libs.versions.toml`:
- Around line 80-82: The nitrite and nitrite-mvstore entries are missing
explicit version pins which makes consumers brittle; update the
libs.versions.toml so both the nitrite and nitrite-mvstore declarations include
version.ref = "nitrite" (matching the existing nitrite-bom version key) so that
libs.bundles.nitrite works without requiring consumers to import the BOM
separately; locate the entries named nitrite and nitrite-mvstore and add
version.ref = "nitrite" to each.

In
`@modules/app/src/main/java/com/tejpratapsingh/animator/worker/SampleMotionWorker.kt`:
- Around line 71-77: The temp output file in SampleMotionWorker.getOutputFile()
is created in applicationContext.cacheDir which can be evicted before the user
taps the persistent notification; change creation to use a durable location
(e.g., applicationContext.filesDir) or, if you must use cacheDir, copy/move the
final video to filesDir (or the same durable location used by
MotionVideoContainer) inside SampleMotionWorker.onCompleted() before posting the
notification so the notification actions reference a stable file path; update
getOutputFile() and/or implement the copy-to-filesDir step in onCompleted() and
ensure any references to the file (notification intents) point to the durable
file.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt`:
- Around line 76-79: getOutputFile() currently returns a directory from
createProjectFolder() but must return an actual file path (e.g., create a .mp4
file with File.createTempFile() or append a filename to the directory) so
produceVideo() and encoders (jcodec/ffmpeg) receive a real file, not a folder;
also stop relying on volatile ProjectStore.motionProject in
provideCurrentProject() by persisting the project ID via inputData
(setCurrentProject() should store the ID into the WorkRequest input) and have
provideCurrentProject() load the project by that ID from inputData when present
so the worker can reconstruct the correct project after a process restart
(follow the SampleMotionWorker pattern for file creation and inputData usage).

In
`@modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt`:
- Around line 71-79: handleSharedText currently skips when no URL is found
leaving the loader visible and logs using a hardcoded "ShareReceiver" tag;
update handleSharedText to check if links.firstOrNull() is null and in that
branch hide the loading indicator (or call finish()), and show a user-facing
message (Toast or Snackbar) before finishing to avoid infinite spinner, and
replace the hardcoded Log.d tag with the companion object's TAG constant;
reference handleSharedText, extractLinks, metadataViewModel.getMetaData and TAG
when making changes.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt`:
- Around line 36-46: The provideCurrentProject() function has a TOCTOU race:
avoid generating Uuid.random() on every call and ensure
ProjectStore.motionProject is created only once by using a synchronized block
and double-checking the nullable field; specifically, check
ProjectStore.motionProject first, and if null enter a
synchronized(ProjectStore::class) (or a dedicated lock object) then recheck
motionProject, generate id and construct MotionProject only inside that
synchronized block and assign it to ProjectStore.motionProject before returning,
so concurrent callers cannot create different instances.
- Around line 20-23: The createOrSaveProject implementation discards the result
of getById and always calls insert, which causes a UniqueConstraintException for
existing MotionProject ids; replace the unconditional insert with Nitrite's
upsert by calling update(project, true) on the repository returned by
db.getRepository(MotionProject::class.java) inside
DatabaseProvider.createOrSaveProject so the MotionProject is updated when
present and inserted when missing.
🧹 Nitpick comments (18)
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/worker/MotionWorker.kt (1)

115-116: Add KDoc for consistency with the other abstract methods.

Every other abstract method in this class (getMotionVideo, onProgress, onCompleted) has a KDoc block describing its purpose and parameters. Consider adding one here as well — e.g., clarifying that it's called on Dispatchers.IO and that implementations may return either a temp file or a persistent location.

📝 Suggested KDoc
+    /**
+     * Provides the output [File] where the generated video will be written.
+     * Called on [Dispatchers.IO]. Implementations may return a temporary or
+     * a persistent file location.
+     */
     abstract fun getOutputFile(): File
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/adapter/AndroidVideoProducerAdapter.kt (1)

45-46: Implicit global dependency reduces testability.

Fetching motionConfig from a global singleton inside the function makes it harder to unit-test with different configurations. The previous approach of passing it as a parameter was more explicit and testable. Consider whether this trade-off is intentional.

Also note the TOCTOU race in provideCurrentConfig(): MotionConfigStore.motionConfig could become null between the != null check and the !! dereference if set from another thread.

modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt (1)

82-89: Trailing \b in regex may truncate URLs ending with non-word characters.

The word boundary \b at the end of the pattern causes the regex engine to backtrack when a URL ends with characters like /, ), or . — trimming them. For example, https://example.com/path/ would be matched as https://example.com/path. Consider using Android's Patterns.WEB_URL matcher instead, or dropping the trailing \b:

-        val urlRegex = Regex("""\b(?:https?|ftp)://\S+\b""")
+        val urlRegex = Regex("""\b(?:https?|ftp)://\S+""")

Note: without the trailing boundary you may want to trim common trailing punctuation (., ,, )) in a post-processing step if the URL appears mid-sentence.

modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt (2)

46-63: Avoid allocating Gson/GsonBuilder on every call.

Both extension functions create a new Gson instance per invocation. Since Gson is thread-safe and immutable after creation, hoist them into top-level vals to avoid repeated allocation and configuration overhead.

♻️ Proposed refactor — cache Gson instances
+private val serializationGson = Gson()
+
+private val deserializationGson =
+    GsonBuilder()
+        .registerTypeAdapter(
+            VideoAspectRatio::class.java,
+            JsonDeserializer { element, _, _ ->
+                val obj = element.asJsonObject
+                val width = obj.get("width")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'width' in VideoAspectRatio JSON")
+                val height = obj.get("height")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'height' in VideoAspectRatio JSON")
+                val label = obj.get("label")?.asString ?: "${width}x${height}"
+
+                allRatios.find { it.width == width && it.height == height }
+                    ?: Custom(width, height, label)
+            },
+        ).create()
+
-fun VideoAspectRatio.toJsonObject(): JsonObject = Gson().toJsonTree(this).asJsonObject
+fun VideoAspectRatio.toJsonObject(): JsonObject = serializationGson.toJsonTree(this).asJsonObject
 
-fun JsonObject.toVideoAspectRatio(): VideoAspectRatio {
-    val gson =
-        GsonBuilder()
-            .registerTypeAdapter(
-                VideoAspectRatio::class.java,
-                JsonDeserializer { element, _, _ ->
-                    val obj = element.asJsonObject
-                    val width = obj.get("width").asInt
-                    val height = obj.get("height").asInt
-                    val label = obj.get("label").asString
-
-                    allRatios.find { it.width == width && it.height == height }
-                        ?: Custom(width, height, label)
-                },
-            ).create()
-    return gson.fromJson(this, VideoAspectRatio::class.java)
-}
+fun JsonObject.toVideoAspectRatio(): VideoAspectRatio =
+    deserializationGson.fromJson(this, VideoAspectRatio::class.java)

26-44: Consider programmatically deriving the predefined ratios to reduce maintenance burden.

The allRatios list is currently complete and accurate (all 16 sealed subclasses of VideoAspectRatio are included). However, this hand-maintained list is fragile: if new predefined ratios are added to VideoAspectRatio without updating allRatios, the deserializer will silently fall back to Custom instead of matching the canonical instance.

Consider deriving this list programmatically—for example, using VideoAspectRatio::class.nestedClasses with reflection or a generated constant—to eliminate the manual synchronization requirement.

gradle.properties (2)

23-31: These appear to be AGP compatibility flags opting out of stricter defaults.

This is a reasonable migration strategy, but consider adding a comment (e.g., # TODO: migrate to new defaults) so these don't become permanent technical debt. Several of these flags (android.nonFinalResIds=false, android.uniquePackageNames=false, android.r8.strictFullModeForKeepRules=false) disable improvements that should eventually be adopted.


19-19: android.enableJetifier=true is deprecated.

Jetifier has been deprecated since AGP 7.x. If all your dependencies have migrated to AndroidX (which is likely given compileSdk 36), you can safely remove this flag to reduce build overhead.

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt (1)

81-82: Cache provideCurrentConfig() in a local variable to avoid redundant calls.

provideCurrentConfig() is called twice in onMeasure, which runs frequently during layout. A single call stored in a local also guards against a (theoretical) config change between the two reads.

♻️ Proposed fix
-            val desiredWidth = provideCurrentConfig().aspectRatio.width
-            val desiredHeight = provideCurrentConfig().aspectRatio.height
+            val aspectRatio = provideCurrentConfig().aspectRatio
+            val desiredWidth = aspectRatio.width
+            val desiredHeight = aspectRatio.height
modules/app/src/main/java/com/tejpratapsingh/animator/presentation/SampleMotionVideo.kt (1)

24-24: Global mutable config is set before constructing the producer — be aware of thread-safety implications.

setCurrentConfig writes to a shared mutable singleton. If multiple callers (e.g., different workers or activities) invoke sampleMotionVideo concurrently with different configs, they will race on the global state. Consider documenting that this must only be called from a single-threaded entry point, or moving to a scoped config approach in the future.

modules/app/src/main/java/com/tejpratapsingh/animator/ui/view/RenaultCar.kt (1)

51-60: Minor: two calls to provideCurrentConfig() can be consolidated.

Same pattern as in BaseFrameMotionView.onMeasure. While these lambdas likely execute once, caching in a local is cleaner and consistent.

♻️ Proposed fix
+        val currentAspectRatio = provideCurrentConfig().aspectRatio
         contourHeightOf {
-            provideCurrentConfig()
-                .aspectRatio.height
+            currentAspectRatio.height
                 .toYInt()
         }
         contourWidthOf {
-            provideCurrentConfig()
-                .aspectRatio.width
+            currentAspectRatio.width
                 .toXInt()
         }
modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/extensions/ContextExtensions.kt (1)

19-22: exists() check before mkdirs() is redundant.

mkdirs() already returns false without error if the directory exists. The explicit exists() guard can be removed for both directory creations.

♻️ Proposed fix
 fun Context.createProjectFolder(motionProject: MotionProject): File {
     val projectsDirectory: File = applicationContext.filesDir.resolve(PROJECTS_DIR)
-    if (!projectsDirectory.exists()) {
-        projectsDirectory.mkdirs()
-    }
-    val fileDirectory: File = projectsDirectory.resolve(motionProject.id)
-    if (!fileDirectory.exists()) {
-        fileDirectory.mkdirs()
-    }
+    val fileDirectory: File = projectsDirectory.resolve(motionProject.id)
+    fileDirectory.mkdirs()
 
     return fileDirectory
 }
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionVideoProducer.kt (1)

28-43: Removing config from with() makes configuration implicit — document the contract.

Callers must now call setCurrentConfig() before MotionVideoProducer.with(). This implicit ordering dependency is easy to miss and could lead to silent use of default config values. Consider adding a KDoc comment on the with() method noting this prerequisite, or adding a runtime check (e.g., logging a warning if no config has been set).

modules/core/src/main/java/com/tejpratapsingh/motionlib/core/MotionConfig.kt (1)

24-29: provideCurrentConfig() performs two separate volatile reads — use a local variable.

Lines 25 and 26 each perform an independent read of MotionConfigStore.motionConfig. While not currently buggy (the value never reverts to null), this is fragile and the !! is an unnecessary forced unwrap. A single read into a local eliminates the concern.

♻️ Proposed fix
-fun provideCurrentConfig(): MotionConfig =
-    if (MotionConfigStore.motionConfig != null) {
-        MotionConfigStore.motionConfig!!
-    } else {
-        MotionConfig()
-    }
+fun provideCurrentConfig(): MotionConfig =
+    MotionConfigStore.motionConfig ?: MotionConfig()
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt (1)

10-29: Duplicate LyricsResponse class in a different package — potential confusion.

There is already a LyricsResponse in com.tejpratapsingh.lyricsmaker.data.api.model (see relevant snippet at modules/lyrics-maker/.../data/api/model/LyricsResponse.kt, lines 8–45) with a different field set (duration: Float?, no name field, defaults on albumName/instrumental). Having two classes with the same name in sibling packages will cause import confusion and maintenance burden. Consider consolidating or clearly differentiating them (e.g., renaming this one to LrcLibLyricsResponse).

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/motion/LyricsVideoProducer.kt (1)

27-28: Global mutable config is not thread-safe and couples callers to ordering.

setCurrentConfig writes to a shared singleton (MotionConfigStore.motionConfig). If getLyricsVideoProducer and getMultiLyricsVideoProducer (or any other producer factory) are ever invoked concurrently or interleaved, the last writer wins and the other caller silently picks up the wrong config. The previous design of passing config as a parameter to MotionVideoProducer.with(...) was safer.

If the global-config pattern is intentional for this PR, at a minimum document or enforce that callers must be serialized (e.g., single-threaded dispatcher). Preferably, restore parameter-based config injection so each producer is self-contained.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt (1)

83-89: Debug toast "Success ${it.lyrics.size}" appears to be a leftover — remove or replace with meaningful UX.

This toast exposes internal result counts to the user. If it's a debug artifact, remove it; if user feedback is needed on success, consider navigating directly or showing a more meaningful message.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (2)

50-52: Exposing MutableStateFlow directly breaks unidirectional data flow.

query, socialMeta, and selectedLyric are all exposed as MutableStateFlow, allowing any consumer to mutate ViewModel state directly. Consider exposing read-only StateFlow and providing dedicated mutation functions (like searchLyrics already does for _uiState).

♻️ Sketch
-    val query: MutableStateFlow<String> = MutableStateFlow("")
-    val socialMeta: MutableStateFlow<SocialMeta> = MutableStateFlow(SocialMeta())
-    val selectedLyric: MutableStateFlow<LyricsResponse?> = MutableStateFlow(null)
+    private val _query = MutableStateFlow("")
+    val query: StateFlow<String> = _query.asStateFlow()
+
+    private val _socialMeta = MutableStateFlow(SocialMeta())
+    val socialMeta: StateFlow<SocialMeta> = _socialMeta.asStateFlow()
+
+    private val _selectedLyric = MutableStateFlow<LyricsResponse?>(null)
+    val selectedLyric: StateFlow<LyricsResponse?> = _selectedLyric.asStateFlow()
+
+    fun updateQuery(value: String) { _query.value = value }
+    fun updateSocialMeta(value: SocialMeta) { _socialMeta.value = value }
+    fun selectLyric(value: LyricsResponse?) { _selectedLyric.value = value }

42-46: Manual wiring of OkHttp/Gson/ApiService/Repository inside the ViewModel.

Constructing these collaborators directly makes the ViewModel hard to test (no way to inject mocks). Consider using a ViewModelFactory, Hilt, or constructor injection to supply the repository.

Copy link

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
modules/tensorflow-motion-ext/build.gradle (1)

48-54: ⚠️ Potential issue | 🔴 Critical

The Kotlin plugin removal breaks compilation—this module contains 7 Kotlin source files.

Removing the Kotlin Android plugin will prevent the build from compiling the Kotlin files in this module: ImageUtils.kt, TensorFlowImageProcessor.kt, CarBgRemover.kt, TfLiteSegmentationHelper.kt, TiledBackgroundRemover.kt, SuperResolutionProcessor.kt, and ExampleUnitTest.kt. The Kotlin plugin must be retained to compile these sources.

modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt (2)

54-68: ⚠️ Potential issue | 🟠 Major

Activity stuck on loading spinner when intent.action is not ACTION_SEND.

The outer when on intent?.action has no else branch. If the activity is launched with an unexpected or null action, neither metadata fetch nor finish() is called, leaving the user stuck on the loading indicator.

Proposed fix
     private fun findSharedUrl() {
         when (intent?.action) {
             Intent.ACTION_SEND -> {
                 when (intent.type) {
                     "text/plain" -> {
                         handleSharedText(intent)
                     }
 
                     else -> {
                         // Handle other types if needed
                         finish() // Close the activity if the type is unsupported
                     }
                 }
             }
+
+            else -> {
+                finish()
+            }
         }
     }

119-124: ⚠️ Potential issue | 🟡 Minor

Activity left in a dead state after metadata fetch error.

On error, the loading spinner is hidden and a toast is shown, but the activity remains open with no actionable UI. Consider calling finish() after the toast (or providing a retry option) so the user isn't stranded.

Proposed fix (finish after error)
                 is MetaDataResult.Error -> {
                     Log.e(TAG, "Received error", result.error)
                     Toast
                         .makeText(this, "Failed to fetch metadata", Toast.LENGTH_SHORT)
                         .show()
+                    finish()
                 }
modules/pytorch-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

Restore the kotlin-android plugin — this module contains multiple Kotlin source files that require it to compile.

The kotlin-android plugin was removed from the plugins block, but the module contains 9 Kotlin source files (.kt):

  • PyTorchImageProcessor.kt
  • ModelTypes.kt
  • RemoveBg.kt
  • Remover.kt
  • ImageUpscaler.kt
  • SuperRes.kt
  • FileUtils.kt
  • NetUtils.kt
  • ExampleUnitTest.kt

Without the Kotlin plugin, these files will fail to compile. The plugin must be restored.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

79-100: ⚠️ Potential issue | 🟠 Major

runBlocking in a View's init blocks the main thread for network I/O.

This performs synchronous network calls (MusicBrainz lookup + image download) on the UI thread during view construction. This will cause ANRs or significant jank. Consider launching a coroutine and updating the ImageView asynchronously.

🐛 Sketch of an async approach
-        ivAlbumArt.apply {
-            runBlocking {
-                if (image != null) {
+        // Launch async instead of blocking UI thread
+        kotlinx.coroutines.CoroutineScope(Dispatchers.Main).launch {
+            if (image != null) {
                 ...
-                } else {
+            } else {
                 ...
-                }
             }
-        }
+            bitmap?.let { ivAlbumArt.setImageBitmap(it) }
+        }

A proper lifecycle-aware scope (e.g., from the hosting Activity/Fragment) would be even better to avoid leaks.

modules/ffmpeg-motion-ext/src/main/java/com/tejpratapsingh/motionlib/ffmpeg/FfmpegVideoProducerAdapter.kt (1)

148-151: ⚠️ Potential issue | 🟠 Major

Pre-existing: integer division silently truncates audio timing values.

Lines 149-151 use integer division (track.startFrame / fps) which truncates fractional seconds. For example, frame 15 at 30fps yields 0 instead of 0.5. The frameToSeconds helper on line 202 handles this correctly but isn't used here.

Not introduced by this PR, but worth fixing since incorrect audio sync could be hard to debug.

Suggested fix
-                val startSec = track.startFrame / fps
-                val endSec = track.endFrame / fps
-                val delayMs = (track.delayFrame / fps * 1000)
+                val startSec = frameToSeconds(track.startFrame, fps)
+                val endSec = frameToSeconds(track.endFrame, fps)
+                val delayMs = (frameToSeconds(track.delayFrame, fps) * 1000).toLong()
🤖 Fix all issues with AI agents
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt`:
- Around line 68-70: In LrcLibApiServiceImpl's catch blocks (the one wrapping
the network call and the one in the get method), do not swallow
CancellationException: detect and rethrow CancellationException before
converting other errors into Result.failure; e.g., replace the single catch (e:
Exception) with logic that if e is CancellationException then throw e, otherwise
return Result.failure(e), ensuring structured concurrency and proper coroutine
cancellation propagation.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`:
- Around line 83-90: The Success toast in SearchActivity is a debug
placeholder—remove the Toast.makeText(...) inside the LyricsUiState.Success
branch or replace its message with a user-facing string (e.g., "Lyrics loaded"
or localized resource) and ensure any UI feedback is meaningful; also simplify
the when handling by collapsing empty Initial and Loading branches into an else
or handling only Success and Error cases so the when is concise and not left
with no-op branches.

In `@modules/motion-store/build.gradle`:
- Around line 8-10: The compileSdk block uses invalid AGP DSL syntax (compileSdk
{ version = release(36) }); replace it with the standard form used in other
modules — set compileSdk directly (e.g., compileSdk = 36) instead of using
release(36) block, ensuring the compileSdk declaration in the module (the
compileSdk symbol) matches the rest of the project.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/DatabaseProvider.kt`:
- Around line 1-4: This file is missing the Kotlin package declaration which
prevents compilation; add the package declaration "package
com.tejpratapsingh.motionstore" as the very first line of the
DatabaseProvider.kt file (before the import statements) so the DatabaseProvider
top-level declarations/types resolve correctly.
- Around line 9-19: The MVStoreModule instance (storeModule) is configured but
never loaded into the Nitrite builder so the DB stays in-memory; update the
Nitrite builder used to create db (the Nitrite.builder() call that currently
only loads JacksonMapperModule) to also load the configured storeModule via
loadModule(storeModule) so the database uses the on-disk MVStore backing instead
of an in-memory store.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/extensions/ContextExtensions.kt`:
- Around line 18-34: The function Context.createProjectFolder is misnamed and
its KDoc is inaccurate because it returns a temporary .mp4 File, not a
directory; rename the function to createProjectOutputFile (or similar), update
the KDoc to state it creates and returns a temp output .mp4 file inside the
project's internal storage directory, and update the inline comment that
currently says "// Use cacheDir for temp files that can be cleared" to
accurately reflect that fileDirectory (internal filesDir) is used (or switch to
cacheDir if you intend truly ephemeral temp files); ensure all call sites and
imports referencing createProjectFolder are updated to the new name.
🧹 Nitpick comments (13)
modules/lyrics-maker/src/main/res/layout/lyrics_container.xml (1)

70-84: Stale constraint on hidden pb_progress.

pb_progress is now visibility="gone" but still carries layout_constraintBottom_toTopOf="@+id/fake_chart_view" (line 82). Since fake_chart_view's bottom anchor changed from btn_container to parent, this reference is now semantically stale. It's harmless while the view is gone, but if it's ever made visible again, the constraint chain may not behave as expected. Consider updating or removing it for clarity.

modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionVideoProducer.kt (1)

68-69: Misleading comment: says Dispatchers.Default but code uses Dispatchers.IO.

The comment on line 69 says "Use Dispatchers.Default for CPU-bound work" while the coroutine actually runs on Dispatchers.IO. This is pre-existing but now more visible. Consider updating the comment to match the actual dispatcher, or switching to Dispatchers.Default if the work is truly CPU-bound (video frame rendering).

✏️ Suggested comment fix
         withContext(Dispatchers.IO) {
-            // Use Dispatchers.Default for CPU-bound work
+            // Use Dispatchers.IO for file I/O and video production
modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt (1)

82-89: Consider using android.util.Patterns.WEB_URL for more robust URL matching.

The hand-rolled regex works for common cases but may over-capture trailing punctuation (e.g., ), . at end of a sentence) or miss edge cases. Android provides a well-tested pattern for this purpose:

private fun extractLinks(text: String): List<String> {
    val matcher = android.util.Patterns.WEB_URL.matcher(text)
    return buildList {
        while (matcher.find()) { add(matcher.group()) }
    }
}
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt (2)

46-63: Consider caching the Gson instances instead of recreating them on every call.

Both toJsonObject() (line 46) and toVideoAspectRatio() (lines 49-62) allocate a new Gson / GsonBuilder on every invocation. If these are called in a loop (e.g., serializing a list of ratios), this adds unnecessary GC pressure. Extract the configured Gson instance to a companion object or a top-level private val.

♻️ Suggested extraction
+private val aspectRatioGson: Gson =
+    GsonBuilder()
+        .registerTypeAdapter(
+            VideoAspectRatio::class.java,
+            JsonDeserializer { element, _, _ ->
+                val obj = element.asJsonObject
+                val width = obj.get("width")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'width' in VideoAspectRatio JSON")
+                val height = obj.get("height")?.asInt
+                    ?: throw IllegalArgumentException("Missing 'height' in VideoAspectRatio JSON")
+                val label = obj.get("label")?.asString ?: "${width}x${height}"
+
+                allRatios.find { it.width == width && it.height == height }
+                    ?: Custom(width, height, label)
+            },
+        ).create()
+
-fun VideoAspectRatio.toJsonObject(): JsonObject = Gson().toJsonTree(this).asJsonObject
+fun VideoAspectRatio.toJsonObject(): JsonObject = aspectRatioGson.toJsonTree(this).asJsonObject
 
-fun JsonObject.toVideoAspectRatio(): VideoAspectRatio {
-    val gson =
-        GsonBuilder()
-            .registerTypeAdapter(
-                VideoAspectRatio::class.java,
-                JsonDeserializer { element, _, _ ->
-                    val obj = element.asJsonObject
-                    val width = obj.get("width").asInt
-                    val height = obj.get("height").asInt
-                    val label = obj.get("label").asString
-
-                    allRatios.find { it.width == width && it.height == height }
-                        ?: Custom(width, height, label)
-                },
-            ).create()
-    return gson.fromJson(this, VideoAspectRatio::class.java)
-}
+fun JsonObject.toVideoAspectRatio(): VideoAspectRatio =
+    aspectRatioGson.fromJson(this, VideoAspectRatio::class.java)

26-44: Consider using sealed subclass introspection to keep allRatios in sync with VideoAspectRatio definitions.

If a new predefined ratio is added to VideoAspectRatio, this hand-maintained list will silently miss it, causing the deserializer to fall back to Custom instead of resolving the known instance. Since VideoAspectRatio is a sealed class, you could use VideoAspectRatio::class.sealedSubclasses.filter { it.isData && it.objectInstance != null }.mapNotNull { it.objectInstance as? VideoAspectRatio } to build this list automatically. Alternatively, consider adding a comment near this list documenting that new predefined ratios must be added here.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/LyricsActivity.kt (1)

58-66: Global side effect inside a lazy property initializer is fragile.

setCurrentProject(...) mutates global state (ProjectStore.motionProject) as a side effect of lazily initializing video. This creates an implicit ordering dependency — any code that reads the current project before video is accessed will see stale/null state, and the intent to set the project is hidden inside a property getter. Consider moving setCurrentProject(...) to onCreate() to make the lifecycle and ordering explicit.

♻️ Proposed refactor
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setCurrentProject(
+            MotionProject(
+                id = "lyrics",
+                name = song,
+                path = "/lyrics",
+                sdui = null,
+            ),
+        )
+
+        val start = lyrics.minBy { it.frame }.frame
     private val video by lazy {
-        setCurrentProject(
-            MotionProject(
-                id = "lyrics",
-                name = song,
-                path = "/lyrics",
-                sdui = null,
-            ),
-        )
         getLyricsVideoProducer(
modules/3d-filament-renderer/src/main/java/com/tejpratapsingh/motionlib/filamentrenderer/Filament3dView.kt (1)

69-86: Consider caching the config to avoid inconsistent reads.

provideCurrentConfig() is called 4 separate times during initialization. If the global config were mutated between calls, the surface buffer size and viewport could disagree. Caching it in a local variable would be safer and cleaner.

♻️ Suggested refactor
 private fun initializeFilament() {
+    val config = provideCurrentConfig()
     Utils.init()
     surfaceTexture = SurfaceTexture(0)
     surfaceTexture.setDefaultBufferSize(
-        provideCurrentConfig().aspectRatio.width,
-        provideCurrentConfig().aspectRatio.height,
+        config.aspectRatio.width,
+        config.aspectRatio.height,
     )
     surface = Surface(surfaceTexture)
     engine = Engine.create()
     swapChain = engine.createSwapChain(surface, 0)
     scene = engine.createScene()
     view = engine.createView()
     view.scene = scene
     renderer = engine.createRenderer()
     view.viewport =
         Viewport(
             0,
             0,
-            provideCurrentConfig().aspectRatio.width,
-            provideCurrentConfig().aspectRatio.height,
+            config.aspectRatio.width,
+            config.aspectRatio.height,
         )
 }
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt (1)

93-146: Prefer items(list) over items(list.size) with index access.

Using items(state.lyrics) with a lambda parameter is more idiomatic Compose and avoids repeated index-based access. You can also provide a key for stable recomposition.

♻️ Suggested refactor
-                    items(state.lyrics.size) { item ->
-                        Card(
-                            modifier =
-                                Modifier
-                                    .fillMaxWidth()
-                                    .padding(vertical = 4.dp)
-                                    .clickable { onLyricsSelected(state.lyrics[item]) },
+                    items(
+                        count = state.lyrics.size,
+                        key = { state.lyrics[it].trackName + state.lyrics[it].artistName },
+                    ) { item ->
+                        val lyrics = state.lyrics[item]
+                        Card(
+                            modifier =
+                                Modifier
+                                    .fillMaxWidth()
+                                    .padding(vertical = 4.dp)
+                                    .clickable { onLyricsSelected(lyrics) },
modules/core/src/main/java/com/tejpratapsingh/motionlib/core/MotionConfig.kt (1)

24-29: provideCurrentConfig() has a double-read on a volatile field — use a local variable.

Between the != null check (line 25) and the !! dereference (line 26), another thread could theoretically interleave. While nothing currently nullifies the store, this is a fragile pattern. Use a local val to read once.

♻️ Proposed fix
 fun provideCurrentConfig(): MotionConfig =
-    if (MotionConfigStore.motionConfig != null) {
-        MotionConfigStore.motionConfig!!
-    } else {
-        MotionConfig()
-    }
+    MotionConfigStore.motionConfig ?: MotionConfig()
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt (1)

81-82: Cache the config to avoid redundant calls.

provideCurrentConfig() is called twice in the same onMeasure pass. If the store is null, each call allocates a new default MotionConfig(). Capture it in a local val for consistency and to avoid the extra allocation, especially since onMeasure can be called frequently.

Suggested fix
-            val desiredWidth = provideCurrentConfig().aspectRatio.width
-            val desiredHeight = provideCurrentConfig().aspectRatio.height
+            val config = provideCurrentConfig()
+            val desiredWidth = config.aspectRatio.width
+            val desiredHeight = config.aspectRatio.height
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/ConfigParser.kt (1)

7-15: Consider reusing a single Gson instance.

A new Gson() is allocated on every serialization call. If this is called frequently, extract a companion-level or module-level instance.

modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (2)

42-46: Dependencies are hard-wired — consider constructor injection for testability.

OkHttpClient, Gson, LrcLibApiService, and LyricsRepository are all instantiated inline, making it impossible to substitute test doubles. If you plan to test this ViewModel, consider accepting the repository (at minimum) as a constructor parameter via a ViewModelFactory or a DI framework.


90-100: lyrics getter re-parses on every access — consider caching or deriving reactively.

Each property access re-calls LrcHelper.getSyncedLyrics(...) and provideCurrentConfig(). If this getter is called from a composable that recomposes frequently, it will redundantly parse lyrics each time. Consider deriving this as a StateFlow (e.g., via combine/map on selectedLyric) so the parsing happens once per selection change.

Comment on lines +68 to +70
} catch (e: Exception) {
Result.failure(e)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

catch (e: Exception) swallows CancellationException, breaking structured concurrency.

When a coroutine is cancelled (e.g., ViewModel is cleared), CancellationException must propagate for proper cleanup. Catching all Exception and wrapping it in Result.failure silently prevents cancellation. Re-throw CancellationException before handling other errors. The same issue applies to the get method at line 116-118.

Proposed fix (apply to both methods)
+import kotlin.coroutines.cancellation.CancellationException
             } catch (e: Exception) {
+                if (e is CancellationException) throw e
                 Result.failure(e)
             }
📝 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
} catch (e: Exception) {
Result.failure(e)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Result.failure(e)
}
🤖 Prompt for AI Agents
In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt`
around lines 68 - 70, In LrcLibApiServiceImpl's catch blocks (the one wrapping
the network call and the one in the get method), do not swallow
CancellationException: detect and rethrow CancellationException before
converting other errors into Result.failure; e.g., replace the single catch (e:
Exception) with logic that if e is CancellationException then throw e, otherwise
return Result.failure(e), ensuring structured concurrency and proper coroutine
cancellation propagation.

@tejpratap46 tejpratap46 force-pushed the feat/implement-sdui branch 5 times, most recently from 9a65385 to b7ea281 Compare February 22, 2026 19:50
.toJsonTree(
mapOf(
"startFrame" to startFrame,
"endFrame" to startFrame,
Copy link

Choose a reason for hiding this comment

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

CRITICAL: Copy-paste bug — endFrame value is serialized as startFrame

The map key is "endFrame" but the value is startFrame. This means the serialized JSON will always have endFrame == startFrame, silently corrupting any round-trip deserialization of MotionEffect.

Suggested change
"endFrame" to startFrame,
"endFrame" to endFrame,

.toJsonTree(
mapOf(
"startFrame" to startFrame,
"endFrame" to startFrame,
Copy link

Choose a reason for hiding this comment

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

CRITICAL: Copy-paste bug — endFrame value is serialized as startFrame

Same issue as MotionEffectParser: the "endFrame" key maps to startFrame instead of endFrame. Any MotionView serialized with this function will have endFrame == startFrame in the JSON, breaking SDUI playback duration.

Suggested change
"endFrame" to startFrame,
"endFrame" to endFrame,

@kilo-code-bot
Copy link

kilo-code-bot bot commented Feb 23, 2026

Code Review Summary

Status: 2 New Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionEffectParser.kt 12 Copy-paste bug: "endFrame" to startFrame should be "endFrame" to endFrame. Serialized JSON always has endFrame == startFrame, corrupting SDUI data.
modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionViewParser.kt 12 Same copy-paste bug: "endFrame" to startFrame should be "endFrame" to endFrame. Breaks SDUI playback duration for all MotionView instances.
Other Observations (not in diff)

Issues found in changed code that warrant attention but are outside inline-commentable diff lines:

File Observation
gradle/wrapper/gradle-wrapper.properties gradle-9.2.1-bin.zip — Gradle 9.x does not exist as a stable release (latest stable is 8.x). This will cause build failures if the distribution URL is not reachable. Verify this is intentional or correct to gradle-8.13-bin.zip.
gradle/libs.versions.toml agpVersion = "9.0.1" — Android Gradle Plugin 9.0.1 does not exist (latest stable is 8.x). This will cause dependency resolution failures.
gradle/libs.versions.toml kotlinVersion = "2.3.10" — Kotlin 2.3.10 does not exist (latest stable is 2.1.x/2.2.x). This will cause dependency resolution failures.
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/infra/AndroidVideoGenerator.kt bitmapFiles instance variable is only initialized once (if (bitmapFiles == null)). If AndroidVideoGenerator.generateVideo() is called a second time with a different inputDir, stale frame files from the first call will be used silently. Consider resetting bitmapFiles = null at the start of each generateVideo() call.
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionVideoProducer.kt The with() factory no longer sets the global MotionConfig state (the also { MotionConfig.aspectRatio = ... } block was removed). Callers must now call setCurrentConfig() separately before using MotionVideoProducer.with(), otherwise provideCurrentConfig() will return the default config. This is a silent breaking change for existing consumers.
Files Reviewed (28 files)
  • .idea/compiler.xml - 0 new issues
  • .idea/gradle.xml - 0 new issues
  • .idea/misc.xml - 0 new issues
  • build.gradle.kts - 0 new issues
  • gradle.properties - 0 new issues
  • gradle/libs.versions.toml - version concerns noted in Other Observations
  • gradle/wrapper/gradle-wrapper.properties - version concern noted in Other Observations
  • modules/3d-filament-renderer/build.gradle - 0 new issues
  • modules/3d-filament-renderer/src/main/java/com/tejpratapsingh/motionlib/filamentrenderer/Filament3dView.kt - 0 new issues
  • modules/motionlib/build.gradle - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/adapter/AndroidVideoProducerAdapter.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/animation/MotionInterpolator.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/infra/AndroidVideoGenerator.kt - concern noted in Other Observations
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/BaseFrameMotionView.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/IMotionVideoProducer.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionComposerView.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/MotionVideoProducer.kt - concern noted in Other Observations
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/MotionVideoPlayer.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/background/GradientView.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/container/RotatingMotionView.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/custom/video/VideoFrameView.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/ui/effects/SlideRightToLeftEffect.kt - 0 new issues
  • modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/worker/MotionWorker.kt - 0 new issues
  • modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/infra/DatabaseManager.kt - 0 new issues
  • modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt - 0 new issues
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/ConfigParser.kt - 0 new issues
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionEffectParser.kt - 1 CRITICAL issue
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionViewParser.kt - 1 CRITICAL issue
  • modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/VideoAspectRatioParser.kt - 0 new issues

Fix these issues in Kilo Cloud

Copy link

@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: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt (2)

96-103: ⚠️ Potential issue | 🟡 Minor

Hardcoded "ShareReceiver" log tag persists in observerMetaData.

Lines 98 and 102 still use the literal "ShareReceiver" instead of the TAG constant — the same class of issue that was fixed in handleSharedText.

🐛 Proposed fix
-            Log.d("ShareReceiver", "Received result: $result")
+            Log.d(TAG, "Received result: $result")
 ...
-                    Log.d("ShareReceiver", "Received metadata: ${result.metaData}")
+                    Log.d(TAG, "Received metadata: ${result.metaData}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt`
around lines 96 - 103, The observerMetaData() function currently uses the
hardcoded log tag "ShareReceiver" in the metadataViewModel.metadata.observe
lambda (two Log.d calls); change those Log.d invocations to use the class TAG
constant instead (i.e., replace "ShareReceiver" with TAG) so it matches the
handleSharedText fix and ensure the TAG constant (e.g., companion object TAG) is
available in the same class.

110-121: ⚠️ Potential issue | 🟠 Major

Unguarded implicit startActivity will crash if no activity handles ACTIVITY_INTENT_ACTION.

Since ShareReceiverActivity lives in a library module, the consuming app is responsible for registering an activity for ACTIVITY_INTENT_ACTION. If it hasn't (or on Android 11+ where package visibility rules apply), startActivity throws ActivityNotFoundException, crashing the app with no user-facing feedback.

🛡️ Proposed fix
                     val onDone = {
-                        startActivity(
-                            Intent(ACTIVITY_INTENT_ACTION).apply {
-                                putExtra(
-                                    EXTRA_METADATA,
-                                    result.metaData
-                                        .copy(title = binding.tvTitle.text.toString()),
-                                )
-                            },
-                        )
-                        finish()
+                        val intent = Intent(ACTIVITY_INTENT_ACTION).apply {
+                            putExtra(
+                                EXTRA_METADATA,
+                                result.metaData.copy(title = binding.tvTitle.text.toString()),
+                            )
+                        }
+                        try {
+                            startActivity(intent)
+                            finish()
+                        } catch (e: android.content.ActivityNotFoundException) {
+                            Log.e(TAG, "No activity found for action: $ACTIVITY_INTENT_ACTION", e)
+                            Toast.makeText(this, "Unable to continue: no handler found", Toast.LENGTH_SHORT).show()
+                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt`
around lines 110 - 121, The onDone lambda in ShareReceiverActivity calls
startActivity with an implicit Intent using ACTIVITY_INTENT_ACTION without
guarding for no receiver; this can throw ActivityNotFoundException in library
consumers. Fix by resolving/checking the Intent before calling startActivity
(use intent.resolveActivity(packageManager) or
packageManager.queryIntentActivities) and if no handler is found, handle
gracefully (e.g., show a user-facing message via Toast/Snackbar or log and
abort) or wrap startActivity in a try/catch for ActivityNotFoundException;
update the onDone closure around startActivity/Intent(EXTRA_METADATA)
accordingly so the app never crashes when no activity handles
ACTIVITY_INTENT_ACTION.
modules/lyrics-maker/src/main/res/layout/lyrics_container.xml (1)

114-143: ⚠️ Potential issue | 🟡 Minor

ImageButtons inside btn_container are missing android:contentDescription.

btn_previous, btn_play_pause, and btn_next all use android:src only, with no contentDescription. While the container is gone now, the SDUI context of this PR suggests these controls may be surfaced programmatically at runtime. When/if shown, TalkBack will announce them as "unlabelled" or read the resource filename.

♿ Proposed fix — add contentDescription to each ImageButton
         <ImageButton
             android:id="@+id/btn_previous"
             ...
+            android:contentDescription="@string/btn_previous_description"
             android:src="@android:drawable/ic_media_previous" />

         <ImageButton
             android:id="@+id/btn_play_pause"
             ...
+            android:contentDescription="@string/btn_play_pause_description"
             android:src="@android:drawable/ic_media_pause" />

         <ImageButton
             android:id="@+id/btn_next"
             ...
+            android:contentDescription="@string/btn_next_description"
             android:src="@android:drawable/ic_media_next" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/lyrics-maker/src/main/res/layout/lyrics_container.xml` around lines
114 - 143, Add accessible labels by setting android:contentDescription on the
ImageButtons with ids btn_previous, btn_play_pause, and btn_next; update the
layout for each ImageButton to include a clear, localized description (e.g.,
"Previous", "Play/Pause", "Next" or string resource references like
`@string/btn_previous_desc`, `@string/btn_play_pause_desc`, `@string/btn_next_desc`)
so TalkBack will announce them correctly when the container is made visible.
modules/pytorch-motion-ext/build.gradle (1)

1-4: ⚠️ Potential issue | 🔴 Critical

kotlin.android plugin must be restored — this module contains 9 Kotlin source files.

The module contains Kotlin files that require the kotlin.android plugin to compile:

  • PyTorchImageProcessor.kt
  • ModelTypes.kt
  • RemoveBg.kt
  • Remover.kt
  • ImageUpscaler.kt
  • SuperRes.kt
  • FileUtils.kt
  • NetUtils.kt
  • ExampleUnitTest.kt

Removing alias(libs.plugins.kotlin.android) without a replacement will break the build.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/pytorch-motion-ext/build.gradle` around lines 1 - 4, The plugins
block removed the Kotlin Android plugin but the module contains Kotlin sources
(e.g. PyTorchImageProcessor.kt, ModelTypes.kt, RemoveBg.kt), so restore the
Kotlin Android plugin by adding back the kotlin.android plugin entry (for
example re-add alias(libs.plugins.kotlin.android) or id
'org.jetbrains.kotlin.android') into the plugins { ... } block alongside
alias(libs.plugins.android.library) and id 'maven-publish' so the Kotlin sources
will compile.
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/worker/MotionWorker.kt (1)

111-113: ⚠️ Potential issue | 🟡 Minor

onFailed should be suspend for consistency with onProgress and onCompleted.

onProgress and onCompleted were made abstract suspend fun, but onFailed remains a plain open fun. Subclasses that need to do async work on failure (e.g., dismiss a foreground notification, log to a remote service) are forced to launch a new coroutine themselves.

♻️ Proposed fix
-    open fun onFailed(exception: Exception) {
+    open suspend fun onFailed(exception: Exception) {
         // Default implementation does nothing, subclasses can override.
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/worker/MotionWorker.kt`
around lines 111 - 113, The onFailed implementation in MotionWorker is
non-suspending while onProgress and onCompleted are suspend functions; change
the signature of open fun onFailed(exception: Exception) to open suspend fun
onFailed(exception: Exception) so subclasses can perform coroutine work directly
(update MotionWorker.onFailed declaration and ensure any subclass overrides are
marked suspend or updated accordingly, e.g., methods overriding onFailed must
now be suspend functions to match the new signature).
modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/RenaultCar.kt (1)

1-1: ⚠️ Potential issue | 🔴 Critical

Add missing package declaration as the first line.

The file is missing its package declaration. It should start with package com.tejpratapsingh.ivi_demo.motion before any imports. Without it, the class lands in the default package, breaking cross-module imports and violating Kotlin conventions. Add this as line 1, with all imports following it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/ivi-demo/src/main/java/com/tejpratapsingh/ivi_demo/motion/RenaultCar.kt`
at line 1, This file is missing its package declaration; add the line "package
com.tejpratapsingh.ivi_demo.motion" as the very first line of RenaultCar.kt so
the RenaultCar class (and any top-level declarations in this file) are placed in
the correct package and imports remain after that declaration.
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/infra/AndroidVideoGenerator.kt (1)

52-54: ⚠️ Potential issue | 🟡 Minor

Remove stale @param motionConfig from KDoc.

The motionConfig parameter was removed from generateVideo, but its KDoc entry was not cleaned up. IDEs and generated docs will show a dangling @param that refers to a non-existent parameter.

🐛 Proposed fix
  * `@param` outputFile The [File] object where the generated video will be saved.
  *                   If an error occurs during generation and this file exists, it will be deleted.
- * `@param` motionConfig A [MotionConfig] object specifying video properties like
- *                     width, height, and frames per second (fps).
  * `@param` motionAudios A list of [MotionAudio] objects to be mixed into the video.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/infra/AndroidVideoGenerator.kt`
around lines 52 - 54, The KDoc for AndroidVideoGenerator.generateVideo still
contains a stale `@param` motionConfig entry even though the motionConfig
parameter was removed; remove the dangling `@param` line from the function's KDoc
(the comment block above the generateVideo method) so the documentation matches
the current signature and no longer references motionConfig.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt (1)

304-304: ⚠️ Potential issue | 🟠 Major

Side-effect callback called directly during composition — use LaunchedEffect.

onSelectionChanged(selected) is invoked in the composition body on every recomposition, not only when selected changes. If the callback triggers any external state mutation (e.g., updating a ViewModel), it can cause an infinite recomposition cycle. Wrap it in LaunchedEffect(selected) so it is only dispatched when the selection actually changes.

🐛 Proposed fix
-        onSelectionChanged(selected)
+        LaunchedEffect(selected) {
+            onSelectionChanged(selected)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt`
at line 304, The call onSelectionChanged(selected) in the SyncedLyricsSelector
composable is a side-effect executed during composition and should be moved into
a LaunchedEffect keyed on selected; modify the SyncedLyricsSelector composable
to remove the direct onSelectionChanged(selected) invocation from the
composition body and instead call onSelectionChanged(selected) inside
LaunchedEffect(selected) (or another appropriate side-effect API) so the
callback runs only when selected actually changes and avoids recomposition
loops.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

102-112: ⚠️ Potential issue | 🔴 Critical

field.first() crashes with NoSuchElementException when selectedLyrics is empty.

The backing field is initialised to emptyList() and any caller that reads selectedLyrics before assigning a non-empty list will hit an unhandled exception.

🐛 Proposed fix
 var selectedLyrics: List<SyncedLyricFrame> = emptyList()
     get() {
+        if (field.isEmpty()) return emptyList()
         val firstFrame = field.first().frame
         return field
             .map {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt`
around lines 102 - 112, The custom getter for selectedLyrics uses field.first()
which throws NoSuchElementException when the backing field is empty; update the
getter in LyricsViewModel (selectedLyrics) to guard against an empty field
(e.g., if (field.isEmpty()) return emptyList()) or use firstOrNull() and return
emptyList() when null, otherwise compute the normalized frames into
SyncedLyricFrame and sort; keep the rest of the mapping logic unchanged so
callers receive an empty list instead of an exception.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/motion/LyricsVideoProducer.kt (1)

29-37: ⚠️ Potential issue | 🟠 Major

lyrics.first() / lyrics.last() throw NoSuchElementException on an empty list.

No guard exists before using lyrics.first() and lyrics.last() to derive startFrame/endFrame. If the caller passes an empty list the function crashes immediately.

🐛 Proposed fix
+    require(lyrics.isNotEmpty()) { "lyrics must not be empty" }
+
     val motionView =
         LyricsContainer(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/motion/LyricsVideoProducer.kt`
around lines 29 - 37, The code uses lyrics.first() and lyrics.last() to set
startFrame/endFrame in the LyricsContainer construction which will throw
NoSuchElementException for an empty lyrics list; update the calling code that
creates LyricsContainer (in LyricsVideoProducer.kt) to guard against an empty
lyrics list by computing safe start/end frames (e.g., use
lyrics.firstOrNull()?.frame and lyrics.lastOrNull()?.frame or provide sensible
default frames) and handle the empty-case (skip creating LyricsContainer, return
early, or supply defaults) so LyricsContainer receives valid startFrame and
endFrame values.
modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/adapter/AndroidVideoProducerAdapter.kt (1)

62-65: ⚠️ Potential issue | 🟡 Minor

Silent error swallowing on frame save failure.

When saving a frame fails, the exception is logged but produceVideo returns the (incomplete/empty) outputFile without signaling the failure to the caller. The caller has no way to distinguish a successful result from a partial failure. Consider throwing or returning a Result type.

This same pattern exists in FfmpegVideoProducerAdapter.kt (line 65).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/adapter/AndroidVideoProducerAdapter.kt`
around lines 62 - 65, In AndroidVideoProducerAdapter (produceVideo) avoid
silently returning outputFile when a frame save fails: instead propagate the
failure by throwing a descriptive exception (or change the method to return a
Result/ sealed outcome) so callers can distinguish partial/failed work; update
the catch block inside produceVideo (the block logging "Error saving frame") to
include throwing a custom exception (or returning Result.failure with the caught
exception) rather than returning outputFile, and apply the same change to the
analogous catch in FfmpegVideoProducerAdapter.kt.
modules/ffmpeg-motion-ext/src/main/java/com/tejpratapsingh/motionlib/ffmpeg/FfmpegVideoProducerAdapter.kt (1)

150-152: ⚠️ Potential issue | 🟠 Major

Integer division truncates audio timing values.

startFrame, endFrame, and delayFrame are divided by fps using integer division, which truncates fractional seconds. For example, frame 13 at 30fps yields 0 seconds instead of 0.433s. This will produce incorrect atrim and adelay values for any audio track not aligned to exact second boundaries.

Proposed fix
             audioTracks.forEachIndexed { index, track ->
-                val startSec = track.startFrame / fps
-                val endSec = track.endFrame / fps
-                val delayMs = (track.delayFrame / fps * 1000)
+                val startSec = track.startFrame.toDouble() / fps
+                val endSec = track.endFrame.toDouble() / fps
+                val delayMs = (track.delayFrame.toDouble() / fps * 1000).toLong()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/ffmpeg-motion-ext/src/main/java/com/tejpratapsingh/motionlib/ffmpeg/FfmpegVideoProducerAdapter.kt`
around lines 150 - 152, The calculations for startSec, endSec and delayMs in
FfmpegVideoProducerAdapter.kt use integer division and truncate fractions;
change the expressions that compute startSec, endSec and delayMs (currently
using track.startFrame / fps, track.endFrame / fps, track.delayFrame / fps *
1000) to perform floating-point division (e.g., convert numerator or fps to
Double/Float) so startSec and endSec are fractional seconds and delayMs is a
precise millisecond Double/Long; update any downstream use (atrim/adelay
formatting) to format these fractional values appropriately.
♻️ Duplicate comments (19)
modules/app/src/main/java/com/tejpratapsingh/animator/worker/SampleMotionWorker.kt (1)

73-81: ⚠️ Potential issue | 🟠 Major

Dispatchers.Unconfined is incorrect for blocking I/O — use Dispatchers.IO.

File.createTempFile is a blocking filesystem call. Dispatchers.Unconfined does not switch threads — it runs the block immediately on the caller's coroutine context, making the withContext wrapper effectively a no-op. This means the blocking call executes on whatever thread the caller happens to be on (potentially the main thread or a limited WorkManager thread).

Use Dispatchers.IO to properly offload the file creation, consistent with MotionVideoContainer.kt which uses Dispatchers.IO for its file operations.

Proposed fix
     override suspend fun getOutputFile(): File =
-        withContext(Dispatchers.Unconfined) {
+        withContext(Dispatchers.IO) {
             File
                 .createTempFile(
                     "motion_video_out_", // More descriptive prefix
                     ".mp4",
                     applicationContext.cacheDir,
                 )
         }

Separate concern — output file in cacheDir may be evicted before the user acts on the notification.

This was flagged in a previous review and remains unaddressed. The OS can purge cache files at any time; the notification's share/open intents will reference a stale path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/app/src/main/java/com/tejpratapsingh/animator/worker/SampleMotionWorker.kt`
around lines 73 - 81, In SampleMotionWorker.override suspend fun
getOutputFile(), replace withContext(Dispatchers.Unconfined) with
withContext(Dispatchers.IO) so the blocking File.createTempFile call runs on the
IO dispatcher (aligns with MotionVideoContainer.kt), and change the temp
directory from applicationContext.cacheDir to a non-evictable location such as
applicationContext.filesDir (or an app-specific external files dir) so the
notification share/open intents don’t point at a file the OS can purge.
modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt (1)

72-84: LGTM! Both issues from the previous review are addressed.

TAG is now used consistently and the no-link branch correctly shows a Toast and calls finish(), eliminating the infinite-spinner case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/metadata-extractor/src/main/java/com/tejpratapsingh/motion/metadataextractor/presentation/ShareReceiverActivity.kt`
around lines 72 - 84, No change needed: the handleSharedText function correctly
logs received text using TAG, extracts links via extractLinks, invokes
metadataViewModel.getMetaData(sharedLink) for the first link, and in the no-link
branch shows a Toast and calls finish(); leave handleSharedText, extractLinks
usage, metadataViewModel.getMetaData, and the no-link Toast+finish behavior
as-is.
modules/jcodec-motion-ext/build.gradle (1)

1-4: Same kotlin.android removal concern — verify no Kotlin sources.

Same pattern as other modules. Refer to the verification comment in modules/metadata-extractor/build.gradle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/jcodec-motion-ext/build.gradle` around lines 1 - 4, The review warns
that the kotlin.android plugin may have been removed from the plugins block;
verify whether this module actually contains any Kotlin sources (check
src/main/kotlin, src/test/kotlin, src/androidTest/kotlin and any Kotlin files
referenced by module classes or test configs) and if Kotlin files exist re-add
the kotlin("android") (or apply plugin: "kotlin-android") declaration to the
plugins block so Kotlin sources are compiled; if there are no Kotlin sources
remove any Kotlin-related config entries (kapt, kotlinOptions, etc.) to avoid
dead config.
modules/3d-opengl-renderer/build.gradle (1)

1-4: Same kotlin.android removal concern — verify no Kotlin sources.

Same pattern as other modules. Refer to the verification comment in modules/metadata-extractor/build.gradle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/3d-opengl-renderer/build.gradle` around lines 1 - 4, The reviewer is
concerned we may have removed the kotlin.android plugin without verifying there
are no Kotlin sources; check the module's source tree for any .kt/.kts files and
references to Kotlin-specific Gradle config (e.g., kotlinOptions, Kotlin
sourceSets, apply plugin: "kotlin-android" or usages of Kotlin JVM/Android
tasks) and if none exist remove any leftover kotlin.android plugin declaration
or related config; if Kotlin sources exist, restore or keep the kotlin.android
plugin and adjust the plugins block (plugins {
alias(libs.plugins.android.library) id 'maven-publish' } ) and any
kotlin-specific config accordingly.
modules/3d-filament-renderer/build.gradle (1)

1-4: Same kotlin.android removal concern — verify no Kotlin sources.

Same pattern as other modules. Refer to the verification comment in modules/metadata-extractor/build.gradle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/3d-filament-renderer/build.gradle` around lines 1 - 4, The plugins
block currently uses alias(libs.plugins.android.library) but the reviewer
flagged removal of a kotlin.android plugin—verify whether this module contains
Kotlin sources (search for .kt files or Kotlin-specific source sets, or
references to Kotlin classes in the module); if there are no Kotlin sources
remove any kotlin.android plugin reference and related Kotlin config, otherwise
keep or add the kotlin(android) plugin (e.g., ensure libs.plugins.kotlin.android
is applied) and update the plugins block so the module compiles (check build
scripts and source dirs referenced by the module to confirm the correct plugin
is present).
modules/sdui/build.gradle (1)

1-4: Same kotlin.android removal concern — verify no Kotlin sources.

Same pattern as other modules. Refer to the verification comment in modules/metadata-extractor/build.gradle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/sdui/build.gradle` around lines 1 - 4, The plugins block now only
contains alias(libs.plugins.android.library) and id 'maven-publish' but the
Kotlin Android plugin removal must be safe; verify there are no Kotlin sources
before removing kotlin.android: search the module for .kt/.kts files and for
src/*/kotlin directories and check Gradle/Kotlin DSL usages (build scripts, test
sources, and any generated Kotlin code). If any Kotlin files or kotlin source
sets exist, re-add the kotlin.android plugin; if none exist, proceed with the
removal and ensure no kotlin-specific config (sourceSets, kotlinOptions) remains
in this module’s Gradle config.
modules/motion-store/build.gradle (1)

1-4: Past concerns about missing kotlin-android plugin and invalid compileSdk syntax are now resolved.

AGP 9.0 introduces built-in Kotlin support and enables it by default — org.jetbrains.kotlin.android no longer needs to be applied to compile Kotlin source files. The compileSdk 36 syntax is also now correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/motion-store/build.gradle` around lines 1 - 4, The previous concerns
about missing kotlin-android and compileSdk syntax are resolved; keep the
plugins block using alias(libs.plugins.android.library) and id 'maven-publish'
as-is, and do not add org.jetbrains.kotlin.android (kotlin-android) back since
AGP 9+ provides built-in Kotlin support; ensure no redundant kotlin plugin is
applied elsewhere and leave the compileSdk declaration using the modern integer
syntax (e.g., compileSdk 36) intact.
gradle/libs.versions.toml (1)

21-23: Past concern about Kotlin 2.3.0 + AGP 9.0.0 incompatibility is resolved.

The upgrade to kotlinVersion = "2.3.10" addresses the prior review comment. Note the separate AGP patch-version finding above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/libs.versions.toml` around lines 21 - 23, The previous incompatibility
is resolved by bumping kotlinVersion to "2.3.10"; keep the updated entries
espresso_core_version, agpVersion, and kotlinVersion as-is and remove the stray
"[duplicate_comment]" marker from the file so the TOML contains only the version
keys (espresso_core_version, agpVersion, kotlinVersion) without the duplicate
note.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt (1)

68-70: catch (e: Exception) still swallows CancellationException, breaking structured concurrency.

Also applies to: 116-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/client/LrcLibApiServiceImpl.kt`
around lines 68 - 70, The current catch blocks in LrcLibApiServiceImpl (where
you return Result.failure(e)) catch Exception and thus swallow
CancellationException; modify those catch clauses so CancellationException is
rethrown (e.g., use catch (e: Exception) { if (e is CancellationException) throw
e; return Result.failure(e) } or use catch (e: CancellationException) { throw e
} plus a separate catch for other Exceptions) to preserve structured concurrency
while still converting other exceptions to Result.failure; update both
occurrences surrounding the try that calls Result.failure to follow this
pattern.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt (1)

586-591: Integer division still truncates frame times; provideCurrentConfig() still called per item.

line.frame / provideCurrentConfig().fps performs integer division (Int / Int), so frames shorter than one fps interval display as 0 sec. Additionally, provideCurrentConfig() is invoked on every list item during every recomposition instead of receiving the fps value already hoisted at line 183.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SyncedLyricsSelector.kt`
around lines 586 - 591, The displayed seconds use integer division and call
provideCurrentConfig() per item; change the Text content to compute a
floating-point seconds value using line.frame.toDouble() / fps (use the hoisted
fps variable already available) and format it (e.g., to 2 decimal places)
instead of calling provideCurrentConfig() inside each item, so replace
"[${line.frame / provideCurrentConfig().fps} sec]" with a formatted string built
from line.frame.toDouble() / fps.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt (1)

7-7: Unused import kotlinx.serialization.Serializable is still present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt`
at line 7, Remove the unused import kotlinx.serialization.Serializable from
LyricsResponse.kt; locate the import statement for
kotlinx.serialization.Serializable and delete it (or if the data class/model
LyricsResponse is intended to be serializable, annotate the class with
`@Serializable` and keep the import), ensuring there are no remaining references
to kotlinx.serialization.Serializable after the change.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt (1)

153-159: "Success ${lyricsState.lyrics.size}" toast is a debug placeholder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`
around lines 153 - 159, Replace the debug placeholder toast in SearchActivity's
LyricsUiState.Success branch: instead of Toast.makeText(this@SearchActivity,
"Success ${lyricsState.lyrics.size}", ...), show a user-facing message or update
the UI appropriately (e.g., display a localized success string or update the
RecyclerView/empty-state based on lyricsState.lyrics.isEmpty). Modify the
LyricsUiState.Success handling (in SearchActivity) to remove the debug
interpolation of lyricsState.lyrics.size and use a proper string resource or UI
update method so users see meaningful feedback.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt (1)

87-88: selectedSongName still produces "null - null" when selectedLyric is unset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/viewmodel/LyricsViewModel.kt`
around lines 87 - 88, selectedSongName currently concatenates nullable
properties directly from selectedLyric.value causing "null - null" when unset;
update the getter to handle nulls by returning a safe placeholder or empty
string. Locate the selectedSongName property in LyricsViewModel and change the
expression using safe operators (e.g., selectedLyric.value?.trackName ?: "" and
selectedLyric.value?.artistName ?: "") or compute via selectedLyric.value?.let {
"${it.trackName} - ${it.artistName}" } ?: "" so it never yields "null - null".
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt (1)

89-97: songDetails[1] still throws IndexOutOfBoundsException when songName has no " - " separator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/view/LyricsContainer.kt`
around lines 89 - 97, The code in LyricsContainer.kt splits songName into
songDetails and directly accesses songDetails[1], which throws
IndexOutOfBoundsException when there is no " - " separator; update the logic in
the block that prepares url (the lines using songName, songDetails, and calling
repository.getAlbumArtUrl) to safely handle missing separator by either using
split with a limit and checking songDetails.size before accessing [1], or
deriving artist/title with safe fallbacks (e.g., treat entire songName as title
and pass an empty/unknown artist) before calling repository.getAlbumArtUrl, and
only call setImageBitmap when repository.getAlbumArtBitmap returns a bitmap.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt (1)

149-179: Error state is silently swallowed by the else branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/SearchLyricsCompose.kt`
around lines 149 - 179, The current else branch in SearchLyricsCompose.kt
silently swallows the error state instead of showing it; update the UI state
handling in the composable that uses recentSearches so you explicitly handle the
error case (e.g., when(viewModel.uiState) { is Error -> show an error Text or
retry Button }, is Loading -> show progress, is Success/Empty -> show
recentSearches LazyColumn). Locate and modify the block that builds the recent
searches list (references: recentSearches, viewModel.query.tryEmit,
viewModel.searchLyrics, LazyColumn) to replace the generic else with explicit
branches for the Error state and render an appropriate error message/action.
Ensure keyboardController hiding and click behavior remain in the recent item
click handler.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt (2)

69-79: fetchBitmap is still missing exception handling — still outstanding from previous review.

client.newCall(request).execute() can throw IOException on network failure; without a try/catch the exception propagates to the caller instead of returning null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt`
around lines 69 - 79, The fetchBitmap function should catch network/IO
exceptions so failures return null instead of propagating; wrap the
client.newCall(request).execute() and the response handling inside a try/catch
for IOException (and optionally Exception), return null from the catch block,
and optionally log the error via your logger; ensure you still use response.use
{ ... } and BitmapFactory.decodeByteArray(bytes, 0, bytes.size) inside the try
so resources are closed correctly.

31-34: Unescaped Lucene special characters in query — still outstanding from previous review.

trackName and artistName are still interpolated without Lucene escaping. Characters like ", +, -, (, ), \ will malform the query.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/albumart/client/AlbumArtRemoteDataSourceImpl.kt`
around lines 31 - 34, The query string in AlbumArtRemoteDataSourceImpl is
interpolating trackName and artistName directly into a Lucene query (the
addQueryParameter call building "recording:\"$trackName\" AND
artist:\"$artistName\""), which allows unescaped Lucene special characters to
break the query; add a helper (e.g., escapeLuceneSpecialChars(String) or
similar) that escapes Lucene special characters (e.g., + - && || ! ( ) { } [ ] ^
" ~ * ? : \ / and whitespace rules) by prefixing them with a backslash, apply
this helper to trackName and artistName before building the query, and use the
escaped values in the addQueryParameter call to ensure well-formed queries.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt (1)

92-96: In-memory ProjectStore state is lost on WorkManager process restart — still outstanding from previous review.

If the process is killed while work is enqueued, WorkManager restarts the worker in a fresh process where ProjectStore.motionProject is null, causing provideCurrentProject() to generate a new random-UUID project disconnected from the intended one. The project ID should be persisted in inputData.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt`
around lines 92 - 96, The worker loses in-memory ProjectStore.motionProject on
process restart; persist and restore the project ID via WorkManager inputData so
provideCurrentProject() doesn't create a new project: when scheduling the work
write the current project's UUID into the WorkRequest inputData, then in
LyricsMotionWorker.getOutputFile() (or inside provideCurrentProject()) read that
projectId from inputData and use it to rehydrate or fetch the existing
motionProject instead of letting provideCurrentProject() generate a new random
UUID; ensure
createProjectFile(applicationContext.createProjectFile(motionProject)) receives
the restored project instance.
modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt (1)

24-32: TOCTOU race in provideCurrentProject — still outstanding from previous review.

UUID.randomUUID().toString() is evaluated as a default argument on every call even when ProjectStore.motionProject is already populated. The read-check-set sequence on the @Volatile field is also non-atomic: two concurrent callers can both observe null and create distinct MotionProject instances with different UUIDs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/tables/MotionProject.kt`
around lines 24 - 32, The current provideCurrentProject function uses a default
argument UUID.randomUUID().toString() and a non-atomic read-check-set on
ProjectStore.motionProject, causing a TOCTOU race where concurrent callers can
create different MotionProject instances; fix by removing the default UUID
generation from the parameter and instead generate the id only when actually
creating a new MotionProject inside an atomic/locked creation path (e.g.,
synchronize on ProjectStore or use an AtomicReference compare-and-set) so that
the read-check-set for ProjectStore.motionProject in provideCurrentProject is
performed atomically and only one MotionProject (with one UUID) is created.
🟡 Minor comments (7)
modules/metadata-extractor/src/main/res/layout/activity_share_receiver.xml-40-42 (1)

40-42: ⚠️ Potential issue | 🟡 Minor

actionNext is semantically incorrect — tv_title is the only input field in this layout.

android:imeOptions="actionNext" is intended to move focus to the next focusable input. Since tv_title is the sole TextInputEditText in the layout, pressing "Next" on the software keyboard will not navigate anywhere meaningful (the focus may jump to btn_next or simply not respond), leaving the keyboard open. actionDone dismisses the keyboard and better matches the user's intent after entering a title.

🛠️ Proposed fix
-            android:imeOptions="actionNext"
+            android:imeOptions="actionDone"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/metadata-extractor/src/main/res/layout/activity_share_receiver.xml`
around lines 40 - 42, The tv_title TextInputEditText uses
android:imeOptions="actionNext" but it's the only input in the layout; update
the tv_title element to use android:imeOptions="actionDone" instead so the IME
sends a Done action (dismissing the keyboard) after entering the title; locate
the tv_title view in activity_share_receiver.xml and replace the imeOptions
value accordingly.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor

duration: Double silently maps missing/null JSON values to 0.0.

When Gson deserializes a response that omits duration or sets it to null, it assigns the JVM primitive default (0.0) to a non-nullable Double field without any error. The previous Float? = null model surfaced this absence explicitly. If the LRCLib API can return responses without duration (e.g., for some instrumental entries), getReadableDuration() will silently report "00 min and 00 sec" instead of indicating missing data.

Consider keeping duration: Double? = null and updating getReadableDuration() with a null guard, or accepting the implicit default if the field is guaranteed by the API contract.

🛡️ Proposed fix (if null duration is possible)
-    `@SerializedName`("duration")
-    val duration: Double,
+    `@SerializedName`("duration")
+    val duration: Double?,
 fun getReadableDuration(): String {
-    val totalSeconds = duration.toInt()
+    val totalSeconds = (duration ?: 0.0).toInt()
     val minutes = totalSeconds / 60
     val seconds = totalSeconds % 60
     return String.format(Locale.getDefault(), "%02d min and %02d sec", minutes, seconds)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/data/api/lrclib/model/LyricsResponse.kt`
around lines 22 - 23, The model currently declares duration: Double which
silently becomes 0.0 when JSON omits/nulls the field; change the property in
LyricsResponse (duration) to a nullable type (Double? = null) and update
getReadableDuration() to handle null (e.g., return a sentinel like "unknown" or
skip formatting) so missing data is explicit; ensure any callers of
getReadableDuration() or direct accesses to duration are adjusted for the
nullable type (use safe calls or null-checks).
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt-182-182 (1)

182-182: ⚠️ Potential issue | 🟡 Minor

Unnecessary parentheses around retrySeconds - 1.

- has higher precedence than the infix downTo, so retrySeconds - 1 downTo 1 parses identically.

🐛 Proposed fix
-        for (i in (retrySeconds - 1) downTo 1) {
+        for (i in retrySeconds - 1 downTo 1) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`
at line 182, In SearchActivity.kt the for-loop uses unnecessary parentheses in
"for (i in (retrySeconds - 1) downTo 1)"; remove the redundant parentheses so it
reads "for (i in retrySeconds - 1 downTo 1)" (no behavior change) to improve
readability around the expression in the loop within the method containing this
retry logic.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt-179-186 (1)

179-186: ⚠️ Potential issue | 🟡 Minor

negativeButton holds a reference to BUTTON_POSITIVE.

The variable is named negativeButton but is retrieved via DialogInterface.BUTTON_POSITIVE. While the countdown logic is functionally correct (it updates the retry/positive button's text), the misleading name is confusing.

🐛 Proposed fix
-            val negativeButton = errorDialog.getButton(DialogInterface.BUTTON_POSITIVE)
+            val retryButton = errorDialog.getButton(DialogInterface.BUTTON_POSITIVE)
 
             for (i in (retrySeconds - 1) downTo 1) {
                 delay(1000)
                 if (errorDialog.isShowing) {
-                    negativeButton.text = getString(R.string.retry, i)
+                    retryButton.text = getString(R.string.retry, i)
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`
around lines 179 - 186, The local variable negativeButton is misnamed: it is
assigned from errorDialog.getButton(DialogInterface.BUTTON_POSITIVE) inside the
lifecycleScope.launch countdown (using retrySeconds) which is confusing; rename
the variable to positiveButton (or fetch DialogInterface.BUTTON_NEGATIVE if you
really meant the negative action) and update all references in that block (the
variable declaration and the subsequent positiveButton.text assignment) so the
name matches the actual button being manipulated.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt-55-60 (1)

55-60: ⚠️ Potential issue | 🟡 Minor

Wake lock is created but never acquired — dead scaffolding.

wakeLock is initialized at class level but wakeLock.acquire() and wakeLock.release() are never called anywhere, so the CPU can still sleep during rendering. The tag "MyApp::MyWakelockTag" is also a placeholder. Either wire the acquire/release around the render lifecycle or remove the scaffolding until it's needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt`
around lines 55 - 60, The wakeLock is allocated but never used; either remove
the scaffolding or actually acquire/release it around the render lifecycle.
Locate the PowerManager and WakeLock fields (powerManager and wakeLock) in
LyricsMotionWorker and either delete them, or wrap the work execution: call
wakeLock.acquire(timeoutMillis) at the start of the worker’s execution method
(e.g., doWork or startWork) and ensure wakeLock.release() in a finally block or
in onStopped to always release; replace the placeholder tag
"MyApp::MyWakelockTag" with a descriptive tag such as
"${appContext.packageName}:LyricsMotionWorker" and ensure you check
wakeLock.isHeld before releasing to avoid IllegalStateException.
modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt-245-245 (1)

245-245: ⚠️ Potential issue | 🟡 Minor

Wrong TAG constant — copy-paste artifact.

TAG is set to "SampleMotionWorker" inside LyricsMotionWorker, so all logs from this class appear under the wrong tag.

🐛 Proposed fix
-        private const val TAG = "SampleMotionWorker"
+        private const val TAG = "LyricsMotionWorker"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/worker/LyricsMotionWorker.kt`
at line 245, The TAG constant in LyricsMotionWorker is a copy-paste artifact
("SampleMotionWorker") causing incorrect log tagging; update the private const
val TAG in the LyricsMotionWorker class to the correct identifier (e.g.,
"LyricsMotionWorker") or derive it from the class name (e.g., using
LyricsMotionWorker::class.java.simpleName) so logs are emitted under the correct
tag.
modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/dao/BaseDao.kt-88-88 (1)

88-88: ⚠️ Potential issue | 🟡 Minor

Misleading doc: insertOrThrow never returns -1 — it throws.

The KDoc says "Returns the new row ID, or -1 on failure," but insertOrThrow throws an exception on failure rather than returning -1. Update the doc to reflect the actual behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/dao/BaseDao.kt`
at line 88, The KDoc for BaseDao.insert is incorrect: it says "Returns the new
row ID, or -1 on failure" but the implementation calls
SQLiteDatabase.insertOrThrow which throws on failure. Update the KDoc for the
insert(T): Long method to state it returns the new row ID and will throw an
exception (e.g., SQLException) on failure instead of returning -1, and mention
any thrown exceptions if you want to be explicit.

<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
Copy link

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

🏁 Script executed:

#!/bin/bash
# Verify Gradle build files target Java 21 consistently

echo "=== Checking sourceCompatibility / targetCompatibility ==="
rg -n 'sourceCompatibility|targetCompatibility' --type groovy --type kotlin -C 2

echo ""
echo "=== Checking Kotlin jvmTarget ==="
rg -n 'jvmTarget' --type groovy --type kotlin -C 2

echo ""
echo "=== Checking toolchain / kotlin.jvm.toolchain ==="
rg -n 'javaVersion|toolchain' --type groovy --type kotlin -C 2

Repository: tejpratap46/AndroidVideoMotion

Length of output: 5887


Gradle build files do not match the JDK 21 target in .idea/misc.xml—this change must be reverted or all Gradle files must be updated.

The IDE config was changed to JDK 21, but the Gradle build scripts remain on Java 11 (most modules) or Java 8 (modules/templates, modules/motionlib, modules/app). This mismatch will cause developers to see JDK 21 in the IDE while the build targets an older JVM version.

Either revert .idea/misc.xml to align with the actual Gradle config, or update all compileOptions { sourceCompatibility JavaVersion.VERSION_11 } blocks to JavaVersion.VERSION_21 across all modules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.idea/misc.xml at line 3, The .idea ProjectRootManager was changed to
languageLevel="JDK_21"/project-jdk-name="jbr-21" but Gradle still targets older
Java versions; either revert that IDE change or update Gradle compile options.
Restore ProjectRootManager's languageLevel/project-jdk-name back to the previous
JDK if you want to keep Gradle as-is, or update every Gradle module that uses
compileOptions { sourceCompatibility JavaVersion.VERSION_11 } (and any
JavaVersion.VERSION_1_8 entries in modules/templates, modules/motionlib,
modules/app) to JavaVersion.VERSION_21 and corresponding targetCompatibility
entries so the IDE JDK and Gradle build targets match.

Comment on lines +22 to +23
agpVersion = "9.0.1"
kotlinVersion = "2.3.10"
Copy link

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

🌐 Web query:

AGP 9.0.28 Kotlin 2.3 support Android Gradle Plugin compatibility

💡 Result:

  • Yes — Kotlin 2.3 is supported with AGP 9.x starting at AGP 9.0.28. Android’s Kotlin support table explicitly notes: “9.x versions before 9.0.28 don't support Kotlin 2.3.” [1]
  • With AGP 9.0, Kotlin is built in (enabled by default), and you should not apply org.jetbrains.kotlin.android / kotlin-android. AGP 9.0 also has a runtime dependency on Kotlin Gradle Plugin (KGP) 2.2.10 for this built-in Kotlin integration. [2]
  • Kotlin 2.3 also documents that kotlin-android is deprecated with AGP 9.0.0+ because AGP provides built-in Kotlin. [3]

Practical takeaway: If you want Kotlin 2.3 + AGP 9, use AGP ≥ 9.0.28 and migrate to AGP’s built-in Kotlin (remove kotlin-android). [1][2][3]

Sources:
[1] Android Developers — “AGP, D8, and R8 versions required for Kotlin versions” (footnote about 9.0.28) (developer.android.com)
[2] Android Developers — “Android Gradle plugin 9.0.0 …” (built-in Kotlin; KGP 2.2.10 runtime dependency) (developer.android.com)
[3] Kotlin Docs — “Compatibility guide for Kotlin 2.3” (kotlin-android deprecated with AGP 9.0+) (kotlinlang.org)

Citations:


🌐 Web query:

Android Gradle Plugin 9.0.1 Kotlin 2.3.10 compatibility D8 R8

💡 Result:

  • Kotlin 2.3.x (including 2.3.10) requires at least AGP 8.13.2 and R8 8.13.19 (minimums). Google’s compatibility table also notes that R8 9.x versions before 9.0.28 don’t support Kotlin 2.3. [1]
  • Kotlin Multiplatform plugin 2.3.10’s published compatibility range lists AGP up to 9.0.0 (not 9.0.1). So AGP 9.0.1 + Kotlin 2.3.10 is not a “known-good” combo from JetBrains’ matrix. [2]

What to do

  1. Preferred: use an AGP version that bundles an R8/D8 combo known to support Kotlin 2.3 (per Google’s table), or upgrade AGP until it bundles R8 ≥ 9.0.28. [1]
  2. If you must stay on AGP 9.0.1: override the embedded R8 with a newer R8 (≥ 9.0.28) using the documented “replace R8 in AGP” mechanism. [3]

Sources:
[1] Google Android Developers – “AGP, D8, and R8 versions required for Kotlin versions” (incl. Kotlin 2.3 row + note about R8 9.0.28)
[2] JetBrains Kotlin docs – Multiplatform compatibility guide (Kotlin 2.3.10 AGP range)
[3] R8 (googlesource) – “Replacing R8 in Android Gradle plugin” instructions


🌐 Web query:

Android Gradle Plugin release notes Kotlin 2.3 support minimum version

💡 Result:

For Kotlin 2.3 bytecode/metadata support in the Android toolchain, Google’s compatibility table lists:

  • Minimum Android Gradle Plugin (AGP): 8.13.2 (with R8 8.13.19) for Kotlin 2.3. [1]
  • If you’re on AGP 9.x, note the footnote: AGP 9.x versions earlier than 9.0.28 don’t support Kotlin 2.3 (so use ≥ 9.0.28). [1]

Separately (from the Kotlin Gradle plugin side), Kotlin 2.3.0 states the minimum supported AGP is 8.2.2 (compatibility range guidance, not the “minimum required for Kotlin 2.3 class files” table). [2]

Sources:
[1] Android Developers — “AGP, D8, and R8 versions required for Kotlin versions”
[2] Kotlin docs — “What’s new in Kotlin 2.3.0”


AGP 9.0.1 does not support Kotlin 2.3.x — upgrade AGP to 9.0.28 or higher.

AGP 9.x versions before 9.0.28 don't support Kotlin 2.3. The pairing of agpVersion = "9.0.1" with kotlinVersion = "2.3.10" is incompatible and can cause D8/R8 build failures. Upgrade agpVersion to 9.0.28 or later.

Proposed fix
-agpVersion = "9.0.1"
+agpVersion = "9.0.28"
📝 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
agpVersion = "9.0.1"
kotlinVersion = "2.3.10"
agpVersion = "9.0.28"
kotlinVersion = "2.3.10"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/libs.versions.toml` around lines 22 - 23, The agpVersion and
kotlinVersion pairing is incompatible—update the agpVersion value used in
libs.versions.toml (agpVersion = "9.0.1") to at least "9.0.28" (or a newer AGP
9.x release) so it supports kotlinVersion = "2.3.10"; locate the agpVersion
entry in gradle/libs.versions.toml and change it to "9.0.28" (or a later
compatible AGP) to resolve the D8/R8 build failures.

Comment on lines +104 to +116
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
lyricsViewModel.uiState.collect {
handleLyricsSearch(it)
}
}

repeatOnLifecycle(Lifecycle.State.STARTED) {
projectsViewModel.shareEvent.collect {
shareProjectFile(it)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Second repeatOnLifecycle is unreachable — projectsViewModel.shareEvent is never collected.

repeatOnLifecycle suspends the coroutine indefinitely (restarting each time the lifecycle crosses STARTED) and only resumes when the lifecycle reaches DESTROYED. Because both calls are sequential inside the same launch block, the second repeatOnLifecycle at line 111 can never execute, so projectsViewModel.shareEvent is silently dropped and the share-project path through onProjectClick is broken.

🐛 Proposed fix — use separate `launch` blocks
-        lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                lyricsViewModel.uiState.collect {
-                    handleLyricsSearch(it)
-                }
-            }
-
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                projectsViewModel.shareEvent.collect {
-                    shareProjectFile(it)
-                }
-            }
-        }
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                lyricsViewModel.uiState.collect {
+                    handleLyricsSearch(it)
+                }
+            }
+        }
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                projectsViewModel.shareEvent.collect {
+                    shareProjectFile(it)
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`
around lines 104 - 116, The second repeatOnLifecycle is never reached because
both repeatOnLifecycle calls are inside the same lifecycleScope.launch and the
first suspends indefinitely; split the collectors into separate coroutines so
both flows are collected. Specifically, in SearchActivity.kt move the
projectsViewModel.shareEvent.collect block into its own lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
projectsViewModel.shareEvent.collect { shareProjectFile(it) } } } (keeping the
existing lifecycleScope.launch/repeatOnLifecycle for
lyricsViewModel.uiState.collect and handleLyricsSearch(it)) so both collectors
run concurrently.

Comment on lines +174 to +177
}.setPositiveButton(retryText) { dialog, _ ->
dialog.dismiss()
lyricsViewModel.searchLyrics(lyricsViewModel.query.value)
}.show()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Retry call maps query.value to trackName instead of query, inconsistent with the original search.

lyricsViewModel.searchLyrics(lyricsViewModel.query.value) is a positional argument, mapping to the first parameter trackName. The keyboard-triggered search (via SearchLyricsCompose) calls searchLyrics(query = searchQuery) using the q free-text field. On retry, this difference causes the LrcLib API to search by exact track name rather than by the same free-text query, which can return different results.

🐛 Proposed fix
-                    lyricsViewModel.searchLyrics(lyricsViewModel.query.value)
+                    lyricsViewModel.searchLyrics(query = lyricsViewModel.query.value)

Apply the same fix at line 192 (the auto-retry after the countdown).

📝 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
}.setPositiveButton(retryText) { dialog, _ ->
dialog.dismiss()
lyricsViewModel.searchLyrics(lyricsViewModel.query.value)
}.show()
}.setPositiveButton(retryText) { dialog, _ ->
dialog.dismiss()
lyricsViewModel.searchLyrics(query = lyricsViewModel.query.value)
}.show()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/activity/SearchActivity.kt`
around lines 174 - 177, Retry currently calls
lyricsViewModel.searchLyrics(lyricsViewModel.query.value) which passes the value
as the positional first parameter (trackName) causing a different search
behavior; change the retry calls (both in the dialog positive button and the
auto-retry after countdown) to call the ViewModel using the named parameter used
by the keyboard-triggered search (e.g., lyricsViewModel.searchLyrics(query =
lyricsViewModel.query.value)) so the free-text q field is used consistently;
update both references (the dialog block using setPositiveButton and the
auto-retry block around line 192) to use the named argument searchLyrics(query =
...) instead of the positional call.

var showDeleteDialog by remember { mutableStateOf(false) }

val context = LocalContext.current
val thumbnail: Bitmap? = remember(project.id) { extractFirstFrame(context.createProjectFile(project).path) }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

extractFirstFrame performs disk + media I/O on the main/composition thread.

remember(project.id) { extractFirstFrame(...) } runs synchronously during composition. MediaMetadataRetriever.setDataSource and getFrameAtTime are both blocking I/O operations that can cause noticeable jank, especially in a grid with many projects.

Move this to a LaunchedEffect with Dispatchers.IO or use produceState:

Proposed fix using produceState
-    val thumbnail: Bitmap? = remember(project.id) { extractFirstFrame(context.createProjectFile(project).path) }
+    val thumbnail: Bitmap? by produceState<Bitmap?>(initialValue = null, project.id) {
+        value = withContext(Dispatchers.IO) {
+            extractFirstFrame(context.createProjectFile(project).path)
+        }
+    }

This requires adding imports for produceState, withContext, and Dispatchers:

import androidx.compose.runtime.produceState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/lyrics-maker/src/main/java/com/tejpratapsingh/lyricsmaker/presentation/compose/ProjectsScreenCompose.kt`
at line 236, The composition currently calls extractFirstFrame synchronously via
remember(project.id) which performs blocking disk/media I/O
(MediaMetadataRetriever) on the main/composition thread; change this to run off
the main thread by replacing the remember(...) usage with a coroutine-based
state producer (e.g., produceState or LaunchedEffect) that calls
extractFirstFrame inside a withContext(Dispatchers.IO) block and assigns the
resulting Bitmap to the composable state; update imports for
produceState/LaunchedEffect, withContext, and Dispatchers, and ensure you still
key by project.id when launching the work so thumbnails load asynchronously
without janking UI.

Comment on lines +46 to +60
// Default: drop all tables and recreate
// Disable FK constraints during migration to avoid constraint errors on drop
db.execSQL("PRAGMA foreign_keys = OFF")
schemas.forEach { schema ->
val tableName =
schema
.substringAfter("CREATE TABLE IF NOT EXISTS ", "")
.substringAfter("CREATE TABLE ", "")
.substringBefore(" ")
.trim()
if (tableName.isNotEmpty()) db.execSQL("DROP TABLE IF EXISTS $tableName")
}
db.execSQL("PRAGMA foreign_keys = ON")
onCreate(db)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PRAGMA foreign_keys = OFF/ON is a no-op inside the onUpgrade transaction.

PRAGMA foreign_keys is a no-op within a transaction; foreign key constraint enforcement may only be enabled or disabled when there is no pending BEGIN or SAVEPOINT. Android's SQLiteOpenHelper wraps onUpgrade in an explicit beginTransaction() / endTransaction() block, so both PRAGMA calls are silently ignored. FK constraints remain active (enforced by onConfigure). Once the table-name extraction bug above is fixed, any DROP TABLE against a parent table with FK-referenced children will throw a constraint error.

The workaround is to set PRAGMA foreign_keys = OFF before the transaction begins — but SQLiteOpenHelper owns that transaction boundary. A practical alternative for the default upgrade path is to use PRAGMA defer_foreign_keys = ON at the start of onUpgrade, which defers FK checks until the transaction commits:

🐛 Suggested approach
     override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
         if (onUpgradeCallback != null) {
             onUpgradeCallback.invoke(db, oldVersion, newVersion)
         } else {
-            db.execSQL("PRAGMA foreign_keys = OFF")
+            // foreign_keys pragma is a no-op inside a transaction (SQLiteOpenHelper wraps
+            // onUpgrade in BEGIN/END). Use defer_foreign_keys instead — it defers FK
+            // checks to commit time so mid-transaction drops of parent tables don't fail.
+            db.execSQL("PRAGMA defer_foreign_keys = ON")
             schemas.forEach { schema ->
                 // ... extract tableName and DROP ...
             }
-            db.execSQL("PRAGMA foreign_keys = ON")
             onCreate(db)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/infra/DatabaseManager.kt`
around lines 46 - 60, The PRAGMA foreign_keys toggles are no-ops inside the
onUpgrade transaction; change the migration to call db.execSQL("PRAGMA
defer_foreign_keys = ON") at the start of onUpgrade (before any DROP/CREATE) and
remove the db.execSQL("PRAGMA foreign_keys = OFF") / ON calls; keep the existing
loop that computes tableName from schemas and perform DROP TABLE IF EXISTS while
relying on defer_foreign_keys so foreign key checks are deferred until commit.
Ensure the defer PRAGMA is executed in the same onUpgrade method (before any
DDL) and that onCreate(db) still runs after the drops.

Comment on lines +49 to +57
schemas.forEach { schema ->
val tableName =
schema
.substringAfter("CREATE TABLE IF NOT EXISTS ", "")
.substringAfter("CREATE TABLE ", "")
.substringBefore(" ")
.trim()
if (tableName.isNotEmpty()) db.execSQL("DROP TABLE IF EXISTS $tableName")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Table name extraction always produces an empty string — DROP TABLE never executes in the default upgrade path.

The two chained .substringAfter(..., "") calls are mutually exclusive rather than cascading:

  • For "CREATE TABLE IF NOT EXISTS foo (...)":
    Step 1 → "foo (...)", Step 2 → "" (because "CREATE TABLE " is not found in "foo (...)"; the missing-delimiter default "" is returned).
  • For "CREATE TABLE foo (...)":
    Step 1 → "" (prefix not found), Step 2 → "" (not found in "").

In both cases tableName is always "", so db.execSQL("DROP TABLE IF EXISTS $tableName") is never reached. When onUpgrade then calls onCreate, every CREATE TABLE IF NOT EXISTS is silently skipped because the old tables still exist. Any schema version bump produces a silent no-op migration, leaving the DB stuck on the old schema.

🐛 Proposed fix
-                val tableName =
-                    schema
-                        .substringAfter("CREATE TABLE IF NOT EXISTS ", "")
-                        .substringAfter("CREATE TABLE ", "")
-                        .substringBefore(" ")
-                        .trim()
-                if (tableName.isNotEmpty()) db.execSQL("DROP TABLE IF EXISTS $tableName")
+                val afterKeyword = when {
+                    schema.contains("CREATE TABLE IF NOT EXISTS ") ->
+                        schema.substringAfter("CREATE TABLE IF NOT EXISTS ")
+                    schema.contains("CREATE TABLE ") ->
+                        schema.substringAfter("CREATE TABLE ")
+                    else -> ""
+                }
+                val tableName = afterKeyword.substringBefore("(").trim()
+                    .split(Regex("\\s+")).firstOrNull().orEmpty()
+                if (tableName.isNotEmpty()) db.execSQL("DROP TABLE IF EXISTS $tableName")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motion-store/src/main/java/com/tejpratapsingh/motionstore/infra/DatabaseManager.kt`
around lines 49 - 57, The table-name extraction in the schemas.forEach block
incorrectly chains substringAfter calls so tableName becomes empty; update the
logic in the schemas.forEach (where tableName is computed and used in
db.execSQL("DROP TABLE IF EXISTS $tableName")) to robustly parse both "CREATE
TABLE IF NOT EXISTS <name>" and "CREATE TABLE <name>" cases — e.g., first check
for "CREATE TABLE IF NOT EXISTS" and fallback to "CREATE TABLE", then
substringBefore the first whitespace/parenthesis and trim to produce a non-empty
tableName before calling db.execSQL; ensure this change fixes the onUpgrade path
that later calls onCreate.

Comment on lines 12 to 16
suspend fun produceVideo(
context: Context,
outputFile: File,
progressListener: ((progress: Int, bitmap: Bitmap) -> Unit)? = null,
progressListener: (suspend (progress: Int, bitmap: Bitmap) -> Unit)? = null,
): File
Copy link

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

🏁 Script executed:

fd -type f -name "MotionVideoContainer.kt"

Repository: tejpratap46/AndroidVideoMotion

Length of output: 244


🏁 Script executed:

cat -n modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/IMotionVideoProducer.kt

Repository: tejpratap46/AndroidVideoMotion

Length of output: 730


🏁 Script executed:

fd -type f -name "IMotionVideoProducer.kt"

Repository: tejpratap46/AndroidVideoMotion

Length of output: 244


🌐 Web query:

Kotlin suspend function type compatibility implicit coercion non-suspend function type

💡 Result:

In Kotlin, suspend function types are not compatible with (and are not subtypes of) non-suspend function types, so there is no implicit coercion between them.

  • A suspending function has type suspend (A1, …, An) -> R, and per the Kotlin spec this type is “unrelated by subtyping to any non-suspending function type”. So you can’t assign/cast a suspend (T) -> R to (T) -> R (or vice‑versa) without an explicit wrapper. (kotlinlang.org)
  • This is why let (which expects (T) -> R) rejects a reference to a suspend function: it’s a plain type mismatch, not something the compiler will adapt. (stackoverflow.com)
  • Lambdas are special only in that they can be inferred as suspend or non-suspend based on context, because they don’t have an explicitly declared function type until inference picks one. But without a suspend-typed expected type, the compiler won’t “upgrade” a lambda to suspend just because it contains a suspend call. (kotlinlang.org)
  • Related “escape hatch”: non-suspending inline lambda parameters may still be allowed to contain suspension points when the inline function is called from a suspending function (a specific exception in the coroutines/type-coloring rules). (kotlinlang.org)

Practical consequence: if an API expects (T) -> R, you must either (1) use a non-suspend function, or (2) change/use an API that accepts suspend (T) -> R, or (3) explicitly wrap/bridge (e.g., call the suspend function inside a coroutine/runBlocking, depending on your threading needs).

Citations:


🏁 Script executed:

find . -name "MotionVideoContainer.kt"

Repository: tejpratap46/AndroidVideoMotion

Length of output: 161


🏁 Script executed:

rg -t kotlin "produceVideo" --context 5

Repository: tejpratap46/AndroidVideoMotion

Length of output: 18938


🏁 Script executed:

find . -type f -name "*.kt" | xargs grep -l "progressListener"

Repository: tejpratap46/AndroidVideoMotion

Length of output: 854


🏁 Script executed:

sed -n '1,160p' ./modules/app/src/main/java/com/tejpratapsingh/animator/ui/view/MotionVideoContainer.kt

Repository: tejpratap46/AndroidVideoMotion

Length of output: 4448


MotionVideoContainer.kt passes a non-suspend lambda to IMotionVideoProducer.produceVideo(), which expects a suspend function type—this is a compilation error.

MotionVideoContainer.kt (line 120) declares progressListener: ((progress: Int, bitmap: Bitmap) -> Unit)? and passes it to IMotionVideoProducer.produceVideo() (line 136), which now expects (suspend (progress: Int, bitmap: Bitmap) -> Unit)?. Per the Kotlin spec, suspend and non-suspend function types are distinct and not implicitly coercible; this will fail at compile time.

Fix MotionVideoContainer.kt by either:

  • Declaring progressListener parameter as (suspend (progress: Int, bitmap: Bitmap) -> Unit)?, or
  • Wrapping the non-suspend lambda to satisfy the suspend type requirement.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/motionlib/src/main/java/com/tejpratapsingh/motionlib/core/motion/IMotionVideoProducer.kt`
around lines 12 - 16, MotionVideoContainer passes a non-suspend lambda to
IMotionVideoProducer.produceVideo which expects a suspend function type; change
the MotionVideoContainer.progressListener declaration to be suspend ((progress:
Int, bitmap: Bitmap) -> Unit)? or wrap the existing non-suspend listener when
calling produceVideo by creating a suspend wrapper (e.g., { p, b ->
existingListener?.invoke(p, b) }) so the passed value matches the suspend
signature expected by IMotionVideoProducer.produceVideo.

Comment on lines +10 to +13
mapOf(
"startFrame" to startFrame,
"endFrame" to startFrame,
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: endFrame is serialized with the value of startFrame.

Line 12 maps "endFrame" to startFrame instead of endFrame. This silently produces incorrect JSON where both fields have the same value, losing the actual end-frame data.

🐛 Proposed fix
         mapOf(
             "startFrame" to startFrame,
-            "endFrame" to startFrame,
+            "endFrame" to endFrame,
         ),
📝 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
mapOf(
"startFrame" to startFrame,
"endFrame" to startFrame,
),
mapOf(
"startFrame" to startFrame,
"endFrame" to endFrame,
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionEffectParser.kt`
around lines 10 - 13, In MotionEffectParser.kt fix the map construction where
"endFrame" is incorrectly assigned startFrame: locate the map (in the method
that serializes effects inside MotionEffectParser) and change the mapping key
"endFrame" to use the endFrame variable instead of startFrame so the serialized
JSON contains the actual end-frame value.

.toJsonTree(
mapOf(
"startFrame" to startFrame,
"endFrame" to startFrame,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

endFrame is incorrectly mapped from startFrame — serialized JSON will always have identical values for both fields.

This is a copy-paste bug: "endFrame" to startFrame should read "endFrame" to endFrame. Any consumer that deserializes this JSON (e.g., SduiRenderer / a reverse JsonObject.toMotionView() parser) will receive the wrong end frame, silently breaking animation range logic.

🐛 Proposed fix
-                "endFrame" to startFrame,
+                "endFrame" to endFrame,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/sdui/src/main/java/com/tejpratapsingh/motion/sdui/infra/MotionViewParser.kt`
at line 12, In MotionViewParser.kt there is a mapping that sets the JSON field
"endFrame" to the variable startFrame (copy-paste bug) so serialized objects
will have identical start and end frames; update the mapping to use the correct
variable endFrame (i.e., change the entry that currently maps "endFrame" to
startFrame so it maps "endFrame" to endFrame) so consumers like SduiRenderer or
JsonObject.toMotionView() receive the correct end-frame value.

@tejpratap46 tejpratap46 merged commit f749cee into main Feb 23, 2026
4 of 7 checks passed
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