Skip to content

Implement room screen logic#26

Open
ParovozikThomas wants to merge 3 commits into
mainfrom
ParovozikThomas/feature/implement-room-screen-logic
Open

Implement room screen logic#26
ParovozikThomas wants to merge 3 commits into
mainfrom
ParovozikThomas/feature/implement-room-screen-logic

Conversation

@ParovozikThomas
Copy link
Copy Markdown
Collaborator

This pull request introduces several significant improvements and new features to the mobile app, focusing on expanding local data storage, enhancing authentication, and improving API and network handling. The update includes a database schema upgrade to support assignments and submission results, adds new DAOs and entities, improves token handling with user roles, and enhances API client configuration. Additionally, new utility classes and dependencies are introduced, and a comprehensive OpenAPI contract is added for feedback events.

Database and Local Storage Enhancements:

  • Upgraded SmartJamDatabase to version 6, adding support for AssignmentEntity and SubmissionResultEntity, and included corresponding DAOs (AssignmentDao, SubmissionResultDao). [1] [2]
  • Added AudioFileStore utility for managing assignment and submission audio files on disk.

Authentication and Token Management:

  • Enhanced TokenStorage to persist user roles alongside tokens, and provided methods to save and retrieve the role. [1] [2]
  • Modified AuthAuthenticator to send the stored user role during token refresh and to save the role with new tokens. [1] [2]

API Client and Networking Improvements:

  • Improved API client setup in MainActivity by adding a custom InstantAdapter for date/time serialization, attaching logging and authorization interceptors, and dynamically updating the bearer token when it changes. [1] [2] [3]
  • Integrated new API services (AssignmentsApi, SubmissionsApi) and repositories, and ensured navigation to login on missing refresh token. [1] [2] [3]
  • Added new dependency for image loading (coil-compose).
  • Updated Room dependencies to version 2.7.0-alpha11.

Testing and Permissions:

  • Added an instrumented test to verify user creation and connection seeding in the database.
  • Updated AndroidManifest.xml to request necessary network permissions and allow cleartext traffic.

API Contract and Serialization:

  • Added common-models.yaml OpenAPI contract for feedback event schemas, supporting future integration and validation.
  • Introduced InstantAdapter for proper handling of Instant serialization with Gson.

These changes collectively improve the app's robustness, scalability, and maintainability, especially around data management and authentication.

References: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18]

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

Warning

Review limit reached

@ParovozikThomas, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 9 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 64ac74f0-0699-4cd7-b039-9693d3b5a969

📥 Commits

Reviewing files that changed from the base of the PR and between d5b78cc and 73dc55a.

📒 Files selected for processing (1)
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt
📝 Walkthrough

Walkthrough

Adds OpenAPI API contracts, Room DB schema for assignments/submissions, role-aware auth and client wiring, media caching/storage, RoomRepository, Room UI and ViewModels with pagination, Coil avatar rendering, and an instrumented DB test.

Changes

Assignment & Room Integration

Layer / File(s) Summary
All changes (single checkpoint)
mobile/app/src/main/java/..., mobile/app/src/main/java/..., mobile/app/build.gradle.kts, mobile/app/src/androidTest/...
Comprehensive set: adds OpenAPI specs, bumps Room, adds Coil, manifest network permissions, Instant JSON adapter, AudioFileStore, Room schema (assignments/submission_results), DAOs, TokenStorage role persistence, AuthRepository/AuthAuthenticator updates, ConnectionRepository avatar caching and pagination, new RoomRepository for assignments/submissions with media download/upload helpers, NavGraph/SmartJamNavGraph updates, Home/Login/Register/Room UI and ViewModels (pagination, uploads, polling, feedback timeline), and an instrumented test seeding connections.

Sequence Diagram(s)

sequenceDiagram
  participant HomeVM
  participant ConnRepo
  participant ApiServer
  participant RoomRepo
  participant DB
  HomeVM->>ConnRepo: syncConnectionsPage(role,page,size)
  ConnRepo->>ApiServer: getMyConnections(page,size)
  ApiServer-->>ConnRepo: connections page DTO
  ConnRepo->>DB: insertConnections(entities with avatar bytes)
  HomeVM->>RoomRepo: syncAssignmentsPage(connectionId,page,size)
  RoomRepo->>ApiServer: getAssignments(connectionId,page,size)
  RoomRepo->>DB: insert/update AssignmentEntity & SubmissionResultEntity
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Satlykovs/SmartJam#17: Related changes carrying role information through refresh flows between client and backend.
  • Satlykovs/SmartJam#16: Prior API client and authenticator wiring that overlaps with MainActivity and AuthAuthenticator changes.
  • Satlykovs/SmartJam#21: Backend assignments/submissions controller and S3 presigned URL implementation aligning with new client RoomRepository APIs.

Suggested labels

OpenAPI

Suggested reviewers

  • Satlykovs

Poem

🐰
I hopped through YAML, tokens in paw,
Cached avatars, audio, and more to jaw,
Roles remembered, pages fetch and flow,
Assignments bloom where the studio grows,
I nibble a carrot — tests all go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Implement room screen logic' aligns with the main changes, which introduce RoomScreen, RoomViewModel, and supporting infrastructure (database, repositories, API services).
Description check ✅ Passed The PR description comprehensively relates to the changeset, covering database enhancements, authentication improvements, API client setup, testing, permissions, and serialization—all present in the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ParovozikThomas/feature/implement-room-screen-logic

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.

@men229
Copy link
Copy Markdown
Collaborator

men229 commented May 21, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Satlykovs
Copy link
Copy Markdown
Owner

@coderabbitai make full rescan of this PR, then create a list of improvements and vulnerabilities/bugs in ENG and RUS

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== AndroidManifest.xml ==="
fd "AndroidManifest.xml" --exec cat {}

echo "=== SmartJamDatabase.kt ==="
fd "SmartJamDatabase.kt" --exec cat {}

echo "=== RoomRepository.kt ==="
fd "RoomRepository.kt" --exec cat {}

echo "=== AuthAuthenticator.kt ==="
fd "AuthAuthenticator.kt" --exec cat {}

echo "=== TokenStorage.kt ==="
fd "TokenStorage.kt" --exec cat {}

echo "=== RoomViewModel.kt ==="
fd "RoomViewModel.kt" --exec cat {}

echo "=== MainActivity.kt ==="
fd "MainActivity.kt" --exec cat {}

echo "=== AudioFileStore.kt ==="
fd "AudioFileStore.kt" --exec cat {}

echo "=== ConnectionEntity.kt ==="
fd "ConnectionEntity.kt" --exec cat {}

echo "=== AssignmentEntity.kt ==="
fd "AssignmentEntity.kt" --exec cat {}

echo "=== SubmissionResultEntity.kt ==="
fd "SubmissionResultEntity.kt" --exec cat {}

Length of output: 40296


@Satlykovs, here is a full analysis of PR #26 with all identified vulnerabilities, bugs, and improvement suggestions — in both English and Russian.


🔴 Vulnerabilities & Security Issues / Уязвимости и проблемы безопасности


1. Cleartext HTTP Traffic Enabled / Разрешён незашифрованный HTTP-трафик

EN: android:usesCleartextTraffic="true" in AndroidManifest.xml allows all HTTP (non-TLS) traffic globally. Any intercepted network traffic (MITM) exposes tokens, audio files, and user data. This should be restricted to debug builds only via a network_security_config.xml.

RU: android:usesCleartextTraffic="true" в AndroidManifest.xml разрешает незашифрованный HTTP для всего приложения. При перехвате трафика (MITM-атака) злоумышленник получит токены, аудиофайлы и данные пользователя. Следует ограничить это только debug-сборками через network_security_config.xml.


2. Tokens Stored in Plain DataStore / Токены хранятся в незашифрованном DataStore

EN: TokenStorage.kt has an explicit //TODO: make encrypted storage comment. Access tokens, refresh tokens, and user roles are stored in plain PreferencesDataStore. An attacker with physical access or root privileges can trivially read these values. Use EncryptedSharedPreferences or the security-crypto DataStore variant.

RU: В TokenStorage.kt присутствует комментарий //TODO: make encrypted storage. Токены доступа, токены обновления и роль пользователя хранятся в незашифрованном PreferencesDataStore. Злоумышленник с физическим доступом или root-правами легко прочитает эти данные. Следует использовать EncryptedSharedPreferences или security-crypto-вариант DataStore.


3. User Role Not Cleared on Logout / Роль пользователя не удаляется при выходе

EN: TokenStorage.clearTokens() removes ACCESS_TOKEN and REFRESH_TOKEN but never removes USER_ROLE. After logout, the stale role persists in DataStore and is re-used on the next token refresh, potentially granting wrong permissions to a new user on the same device.

RU: Метод TokenStorage.clearTokens() удаляет ACCESS_TOKEN и REFRESH_TOKEN, но не удаляет USER_ROLE. После выхода из системы устаревшая роль остаётся в DataStore и используется при следующем обновлении токена — это может привести к тому, что новый пользователь на том же устройстве получит чужую роль.

// Fix / Исправление:
suspend fun clearTokens() {
    context.dataStore.edit { preferences ->
        preferences.remove(ACCESS_TOKEN)
        preferences.remove(REFRESH_TOKEN)
        preferences.remove(USER_ROLE) // ← add this / добавить это
    }
}

4. runBlocking on OkHttp I/O Thread (ANR Risk) / runBlocking в потоке OkHttp (риск ANR)

EN: In MainActivity.kt, the auth interceptor calls runBlocking { tokenStorage.accessToken.first() } on the OkHttp I/O thread. If the DataStore coroutine is slow or blocked, this can deadlock or cause an Application Not Responding (ANR) error. Use a SynchronousQueue / cached token approach instead.

RU: В MainActivity.kt интерцептор авторизации вызывает runBlocking { tokenStorage.accessToken.first() } в потоке OkHttp. Если операция DataStore выполняется медленно или заблокирована, это может вызвать дедлок или ошибку ANR (приложение не отвечает). Следует кэшировать токен в памяти и обновлять его при изменении.


5. Backup Exposes Sensitive Data / Резервное копирование раскрывает конфиденциальные данные

EN: android:allowBackup="true" is set in the manifest. Combined with unencrypted DataStore and audio files in filesDir, Android Auto-Backup can expose tokens and recordings to Google Drive or ADB backups. The data_extraction_rules.xml should explicitly exclude sensitive paths.

RU: В манифесте установлен android:allowBackup="true". В сочетании с незашифрованным DataStore и аудиофайлами в filesDir, механизм Android Auto-Backup может передать токены и записи в Google Drive или через ADB. Необходимо явно исключить чувствительные пути в data_extraction_rules.xml.


🔴 Bugs / Баги


6. URL Replace Order Bug — references.localhost Never Matched / Порядок замены URL — references.localhost никогда не совпадает

EN: In RoomRepository.uploadFileToS3, the replacements are applied in this order:

  1. .replace("localhost", "10.0.2.2") ← runs first
  2. .replace("127.0.0.1", "10.0.2.2")
  3. .replace("references.localhost", "10.0.2.2")never matches because step 1 already converted "localhost""10.0.2.2"

The string "references.localhost" becomes "references.10.0.2.2" after step 1, so step 3 never fires. The references.localhost domain is silently left broken.

RU: В RoomRepository.uploadFileToS3 замены применяются в следующем порядке:

  1. .replace("localhost", "10.0.2.2") ← выполняется первой
  2. .replace("127.0.0.1", "10.0.2.2")
  3. .replace("references.localhost", "10.0.2.2")никогда не совпадает, т.к. шаг 1 уже заменил "localhost" на "10.0.2.2"
// Fix / Исправление — reverse the order:
val fixedUrl = uploadUrl
    .replace("references.localhost", "10.0.2.2") // ← первым
    .replace("localhost", "10.0.2.2")
    .replace("127.0.0.1", "10.0.2.2")

7. ByteArray in data class Breaks equals()/hashCode() / ByteArray в data class ломает equals()/hashCode()

EN: ConnectionEntity is a data class with a peerAvatarBytes: ByteArray? field. Kotlin's auto-generated equals() and hashCode() for data class use referential equality for arrays, not structural equality. Two entities with identical avatar bytes will not be considered equal, which can cause incorrect diff results in Room, Compose recompositions, and unit tests.

RU: ConnectionEntity является data class с полем peerAvatarBytes: ByteArray?. Автоматически генерируемые equals() и hashCode() для data class используют ссылочное сравнение массивов. Два объекта с одинаковыми байтами аватара не будут считаться равными, что приведёт к некорректным diff-результатам в Room, ненужным рекомпозициям в Compose и ошибкам в тестах.


8. Database Version Jumped 1 → 6 Without Migrations / Версия БД прыгнула с 1 до 6 без миграций

EN: SmartJamDatabase went from version = 1 to version = 6. fallbackToDestructiveMigration(dropAllTables = true) is used in MainActivity, meaning all existing user data (connections, etc.) is silently wiped on app update. This is acceptable during development but must be replaced with proper Migration objects before release.

RU: SmartJamDatabase перешла с version = 1 до version = 6. В MainActivity используется fallbackToDestructiveMigration(dropAllTables = true), что означает полную потерю данных пользователя при обновлении приложения. Это приемлемо при разработке, но перед релизом необходимо написать объекты Migration.


9. getSubmissionResult Overwrites createdAt with Instant.now() / Перезаписывает createdAt текущим временем

EN: In RoomRepository.getSubmissionResult, when upserting the SubmissionResultEntity, createdAt is set to java.time.Instant.now() if existing is null. However, existing is fetched from the DB before inserting — it can genuinely be null for a brand-new submission (just created). This means the entity's createdAt will reflect the local cache time, not the server-side creation timestamp, causing incorrect ordering in the UI.

RU: В RoomRepository.getSubmissionResult, при сохранении SubmissionResultEntity в случае отсутствия existing (только что созданная запись) createdAt устанавливается в Instant.now(). Это означает, что время создания будет отражать момент локального кэширования, а не время на сервере, нарушая правильный порядок в UI.


10. Transient null Refresh Token Triggers Navigation to Login / Кратковременный null токена запускает навигацию на экран входа

EN: In MainActivity.kt, LaunchedEffect(startDestination) collects tokenStorage.refreshToken in an infinite loop and navigates to Login whenever the token is null or empty. During token refresh, there is a brief window where the old token is cleared and the new one is not yet saved. This race condition can kick the user to the Login screen mid-session.

RU: В MainActivity.kt, LaunchedEffect(startDestination) бесконечно собирает значения tokenStorage.refreshToken и направляет пользователя на экран входа при null или пустом токене. Во время обновления токена есть краткий момент, когда старый токен уже удалён, а новый ещё не сохранён. Это гонка данных, которая может выбросить пользователя на экран входа в середине сессии.


11. Unbounded Coroutine Spawning Inside collect / Безлимитное создание корутин внутри collect

EN: In RoomViewModel.observeSubmissions, for every emission of the submissions flow, a new viewModelScope.launch is spawned for each submission that needs a detail fetch. If the flow emits rapidly or there are many submissions, this creates a large number of concurrent coroutines with no backpressure or deduplication, potentially causing redundant API calls and memory pressure.

RU: В RoomViewModel.observeSubmissions для каждого нового значения потока (flow) создаются новые корутины viewModelScope.launch для каждой записи, которой нужна детализация. При частых эмиссиях или большом количестве записей это порождает множество параллельных корутин без механизма обратного давления или дедупликации, что ведёт к лишним вызовам API и утечке памяти.


12. RegisterViewModel.isLoading Not Reset on Failure / isLoading не сбрасывается при ошибке регистрации

EN: According to the diff in RegisterViewModel.kt, isLoading = false is only set inside the if (result.isSuccess) branch. On registration failure, isLoading remains true permanently — the spinner never stops and the button is permanently disabled.

RU: Согласно diff в RegisterViewModel.kt, isLoading = false устанавливается только внутри ветки if (result.isSuccess). При ошибке регистрации isLoading навсегда остаётся true — индикатор загрузки не останавливается и кнопка навсегда блокируется.


🟡 Improvements / Улучшения


13. Hardcoded 10.0.2.2 Emulator Address / Хардкод адреса эмулятора 10.0.2.2

EN: RoomRepository rewrites localhost/127.0.0.1 to 10.0.2.2 (Android emulator loopback). This will silently fail on physical devices or CI environments. This logic must be gated behind BuildConfig.DEBUG or moved to configuration.

RU: RoomRepository заменяет localhost/127.0.0.1 на 10.0.2.2 (петлевой адрес эмулятора Android). На реальных устройствах и в CI-средах это тихо сломается. Данную логику следует вынести за условие BuildConfig.DEBUG или в конфигурацию.


14. peerAvatarBytes Stored as Blob in Room / peerAvatarBytes хранится как BLOB в Room

EN: ConnectionEntity.peerAvatarBytes: ByteArray? stores raw avatar image data directly in SQLite. Storing large binary blobs in a relational database degrades query performance and increases memory usage when loading all connections. Avatar bytes should be cached to a file (similar to AudioFileStore) and referenced by path.

RU: ConnectionEntity.peerAvatarBytes: ByteArray? хранит бинарные данные аватара напрямую в SQLite. Хранение больших BLOB-объектов в реляционной БД снижает производительность запросов и увеличивает потребление памяти при загрузке списка соединений. Байты аватара следует кэшировать в файл (аналогично AudioFileStore) и хранить только путь к файлу.


15. No DI Framework / Отсутствует фреймворк внедрения зависимостей

EN: All dependencies (database, repositories, API clients, TokenStorage) are manually wired in MainActivity.onCreate. This makes testing, swapping implementations, and lifecycle management difficult. Integrating Hilt or Koin is strongly recommended.

RU: Все зависимости (база данных, репозитории, API-клиенты, TokenStorage) создаются вручную в MainActivity.onCreate. Это затрудняет тестирование, подмену реализаций и управление жизненным циклом. Настоятельно рекомендуется интегрировать Hilt или Koin.


16. No Timeout on RoomRepository.httpClient / Нет таймаутов на httpClient в RoomRepository

EN: The OkHttpClient created in RoomRepository for audio downloads/uploads has no connectTimeout, readTimeout, or writeTimeout configured. Downloading large audio files from a slow server will hang indefinitely.

RU: OkHttpClient, создаваемый в RoomRepository для скачивания/загрузки аудиофайлов, не имеет настроенных connectTimeout, readTimeout и writeTimeout. Загрузка больших аудиофайлов с медленного сервера будет зависать бесконечно.

private val httpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(120, TimeUnit.SECONDS)
    .writeTimeout(120, TimeUnit.SECONDS)
    .followRedirects(true)
    .build()

17. Partial Download Leaves Corrupted Files / Частичная загрузка оставляет повреждённые файлы

EN: In cacheReferenceAudioIfNeeded and cacheSubmissionAudioIfNeeded, if the download fails mid-stream (exception after the file is opened), a partial/empty file remains on disk. On the next call, the file exists but is invalid, so it won't be re-downloaded. Files should be written to a temp path and renamed atomically on success.

RU: В методах cacheReferenceAudioIfNeeded и cacheSubmissionAudioIfNeeded, если загрузка прерывается в процессе (исключение после открытия файла), на диске остаётся частичный/пустой файл. При следующем вызове файл уже «существует» и не будет скачан повторно. Следует писать во временный файл и переименовывать атомарно по завершении.


18. Status Strings Compared as Hardcoded Literals / Статусы сравниваются как захардкоженные строки

EN: In RoomViewModel.startSubmissionPolling, status is compared as dto.status.name == "COMPLETED". Since dto.status is an enum, comparing against status == AudioProcessingStatus.COMPLETED would be type-safe and refactor-proof.

RU: В RoomViewModel.startSubmissionPolling статус сравнивается как dto.status.name == "COMPLETED". Поскольку dto.status является enum, сравнение status == AudioProcessingStatus.COMPLETED было бы типобезопасным и устойчивым к рефакторингу.


19. submissionsJobs Map Grows Unboundedly / Карта submissionsJobs растёт бесконечно

EN: In RoomViewModel, submissionsJobs: MutableMap<UUID, Job> accumulates one entry per assignment that was ever expanded. There is no cleanup when the ViewModel is cleared or when assignments are removed. Over time this map leaks job references, though jobs are already cancelled; the map itself should be cleared in onCleared().

RU: В RoomViewModel карта submissionsJobs: MutableMap<UUID, Job> накапливает одну запись на каждое когда-либо раскрытое задание. Очистки при вызове onCleared() ViewModel нет. Со временем карта хранит устаревшие ссылки на задания. Следует вызывать submissionsJobs.clear() в onCleared().


20. AuthAuthenticator Creates a New ApiClient Per Refresh / AuthAuthenticator создаёт новый ApiClient при каждом обновлении токена

EN: AuthAuthenticator.authenticate instantiates a brand-new ApiClient (and therefore a new OkHttpClient) on every token refresh attempt. This bypasses connection pooling and adds unnecessary overhead. A dedicated, pre-built ApiClient for auth (without the authenticator itself) should be injected once.

RU: AuthAuthenticator.authenticate создаёт новый экземпляр ApiClient (а значит, и новый OkHttpClient) при каждой попытке обновления токена. Это обходит пул соединений и добавляет лишние накладные расходы. Следует однократно внедрить выделенный ApiClient для аутентификации (без самого AuthAuthenticator).


21. .wav Extension Hardcoded in AudioFileStore / Расширение .wav захардкожено в AudioFileStore

EN: AudioFileStore always creates files with a .wav extension, but the server may return audio in other formats (e.g., .mp3, .ogg). This can cause issues with MediaPlayer/ExoPlayer codec selection and mislead file managers.

RU: AudioFileStore всегда создаёт файлы с расширением .wav, но сервер может возвращать аудио в других форматах (.mp3, .ogg). Это может вызвать проблемы с выбором кодека в MediaPlayer/ExoPlayer и ввести в заблуждение файловые менеджеры.


22. Mixed Russian/English Error Messages / Сообщения об ошибках на разных языках

EN: Error messages are inconsistent — some are in Russian ("Не удалось загрузить следующую страницу"), others in English ("Upload failed", "Creation failed", "Submission creation failed"). All user-facing strings should be moved to strings.xml for proper i18n support.

RU: Сообщения об ошибках непоследовательны — часть на русском («Не удалось обновить список уроков»), часть на английском («Upload failed», «Creation failed»). Все строки, отображаемые пользователю, следует вынести в strings.xml для поддержки i18n.


Summary Table / Сводная таблица

# Severity / Критичность Category / Категория Short Description / Краткое описание
1 🔴 Critical Security Cleartext HTTP globally enabled
2 🔴 Critical Security Tokens in plain DataStore
3 🔴 Critical Bug USER_ROLE not cleared on logout
4 🔴 Critical Bug runBlocking in OkHttp interceptor → ANR
5 🔴 High Security allowBackup exposes data
6 🔴 High Bug references.localhost replace never matches
7 🟠 High Bug ByteArray in data class breaks equality
8 🟠 High Bug DB v1→v6, destructive migration = data loss
9 🟡 Medium Bug createdAt overwritten with Instant.now()
10 🟡 Medium Bug Transient null token → spurious Login redirect
11 🟡 Medium Bug Unbounded coroutines in collect
12 🟡 Medium Bug isLoading stuck true on register failure
13 🟠 High Improvement 10.0.2.2 emulator address in production code
14 🟡 Medium Improvement Avatar bytes as BLOB in Room
15 🟡 Medium Improvement No DI framework
16 🟡 Medium Improvement No HTTP timeouts on audio client
17 🟡 Medium Improvement Corrupted files on partial download
18 🟢 Low Improvement Hardcoded status strings instead of enum
19 🟢 Low Improvement submissionsJobs map not cleaned up
20 🟢 Low Improvement New ApiClient per token refresh
21 🟢 Low Improvement .wav hardcoded in AudioFileStore
22 🟢 Low Improvement Mixed RU/EN error messages
✅ Actions performed

Full review triggered.

@Satlykovs Satlykovs requested review from Satlykovs and men229 May 25, 2026 11:09
@Satlykovs Satlykovs added Moblie Improvements in the mobile app enhancement New feature or request labels May 25, 2026
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (3)
mobile/app/src/main/java/com/smartjam/app/data/local/entity/ConnectionEntity.kt (1)

9-17: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

ByteArray equality in ConnectionEntity.peerAvatarBytes / Connection.peerAvatarBytes is reference-based

ConnectionEntity and the mapped domain Connection are data classes that contain ByteArray?. Kotlin’s generated equals/hashCode compares ByteArray by reference (not byte content), which will be wrong if these objects are ever compared directly, used in sets/maps, or fed into list diffing that relies on equals. Current UI rendering uses Compose LazyColumn(items(state.connections)) and doesn’t show DiffUtil/areContentsTheSame, so this may not affect behavior right now. Consider switching to contentEquals/contentHashCode (or excluding the byte array from equality) if correctness matters for future comparisons.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mobile/app/src/main/java/com/smartjam/app/data/local/entity/ConnectionEntity.kt`
around lines 9 - 17, The data classes ConnectionEntity and Connection currently
hold peerAvatarBytes: ByteArray?, so their auto-generated equals/hashCode
compare the byte arrays by reference; to fix, implement custom equals(other:
Any?) and hashCode() in both ConnectionEntity and Connection that compare
peerAvatarBytes using peerAvatarBytes?.contentEquals(other.peerAvatarBytes)
(handling nulls) and compute hashCode with peerAvatarBytes?.contentHashCode()
combined with other properties, or alternatively change peerAvatarBytes to an
immutable type that compares by content (e.g., List<Byte> or a ByteString) and
update any mapping code that converts between ConnectionEntity and Connection
accordingly.
mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt (2)

84-86: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Error message does not reflect actual password requirements.

The regex at line 40 requires at least one special character from [#?!@$%^&*-], but the error message only mentions "латинские буквы и цифры" (Latin letters and digits). Users won't know they need a special character.

📝 Suggested fix
         if (!passwordRegex.matches(currentState.passwordInput)) {
-            _state.update { it.copy(errorMessage = "Пароль: мин. 8 символов, латинские буквы и цифры") }
+            _state.update { it.copy(errorMessage = "Пароль: 8-20 символов, заглавные и строчные буквы, цифра и спецсимвол (#?!@\$%^&*-)") }
             return
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt`
around lines 84 - 86, The error text shown when passwordRegex fails is
misleading: passwordRegex (defined near line 40) requires at least one special
character from [#?!@$%^&*-] but the _state.update call sets errorMessage to only
mention letters and digits; update the errorMessage in the failing branch (where
_state.update is called) to list the actual requirements (e.g., minimum 8
characters, Latin letters, digits, and at least one special character like
#?!@$%^&*-) so it matches passwordRegex and users know to include a special
character.

104-110: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

isLoading not reset on registration failure — UI will be stuck in loading state.

When registration fails, the failure branch only updates errorMessage but does not reset isLoading to false. This leaves the UI stuck showing the loading indicator with no way for the user to retry.

🐛 Proposed fix
             if (result.isSuccess) {
                 _state.update { it.copy(isLoading = false) }
                 eventChannel.send(RegisterEvent.NavigateToHome)
             } else {
                 val error = result.exceptionOrNull()?.message ?: "Ошибка регистрации"
-                _state.update { it.copy(errorMessage = error) }
+                _state.update { it.copy(errorMessage = error, isLoading = false) }
             }

Alternatively, use a finally block like in LoginViewModel:

+            try {
                 val result = authRepository.register(...)
                 if (result.isSuccess) {
-                    _state.update { it.copy(isLoading = false) }
                     eventChannel.send(RegisterEvent.NavigateToHome)
                 } else {
                     val error = result.exceptionOrNull()?.message ?: "Ошибка регистрации"
                     _state.update { it.copy(errorMessage = error) }
                 }
+            } finally {
+                _state.update { it.copy(isLoading = false) }
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt`
around lines 104 - 110, In RegisterViewModel the failure branch after checking
result.isSuccess only updates errorMessage and never resets isLoading, causing
the UI to remain in a loading state; update the failure branch (the else after
if (result.isSuccess)) to also set _state.update { it.copy(isLoading = false,
errorMessage = error) } or move the isLoading=false logic into a shared
finally/cleanup section (similar to LoginViewModel) so _state.isLoading is
always cleared whether registration succeeds (and you send
RegisterEvent.NavigateToHome) or fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mobile/app/src/main/AndroidManifest.xml`:
- Line 9: The manifest enables cleartext globally via
android:usesCleartextTraffic="true", which weakens transport security; remove
that global attribute and instead create a network security config (e.g.,
res/xml/network_security_config.xml) that enables cleartext only for debug
builds and disallows it for release, then reference that config from the
<application> tag using
android:networkSecurityConfig="`@xml/network_security_config`" via
build-variant-specific manifests or manifest placeholders so cleartext is
allowed only in debug and not in production.

In `@mobile/app/src/main/java/com/smartjam/app/api.yaml`:
- Around line 531-534: The roles array in UserResponse is typed as plain strings
which allows invalid values; change the items definition to reference the
existing enum schema by replacing the current items: { type: string } with an
item $ref to the UserRole schema (referencing components.schemas.UserRole, e.g.
$ref: '`#/components/schemas/UserRole`') so UserResponse.roles uses the UserRole
enum.

In `@mobile/app/src/main/java/com/smartjam/app/common-models.yaml`:
- Around line 44-59: The four timestamp properties teacherStartTime,
teacherEndTime, studentStartTime, and studentEndTime allow negative values;
update their OpenAPI schema entries to enforce non-negative values by adding
"minimum: 0" to each property in the common-models YAML(s) so both copies of the
schema are hardened (ensure you modify the definitions for teacherStartTime,
teacherEndTime, studentStartTime and studentEndTime).

In `@mobile/app/src/main/java/com/smartjam/app/data/api/AuthAuthenticator.kt`:
- Around line 86-90: The toApiRole function is catching Exception too broadly;
narrow it to only catch IllegalArgumentException thrown by
com.smartjam.app.model.UserRole.valueOf(role) so invalid enum names return null
without masking other errors—update the try/catch in toApiRole to catch
IllegalArgumentException (or use runCatching { UserRole.valueOf(role)
}.getOrNull()) and keep the current null-on-failure behavior.

In `@mobile/app/src/main/java/com/smartjam/app/data/api/InstantAdapter.kt`:
- Around line 24-27: InstantAdapter.read() currently swallows all exceptions
from Instant.parse(s) and returns null, which can silently produce non-null Room
fields (AssignmentEntity, ConnectionEntity, SubmissionResultEntity); modify
InstantAdapter.read() to fail fast by not returning null on parse errors—catch
the specific parse exception (e.g., DateTimeParseException) or catch Exception
and rethrow a JsonDataException/IOException with a clear message including the
offending string, so deserialization fails immediately instead of propagating
nulls.

In `@mobile/app/src/main/java/com/smartjam/app/data/local/AudioFileStore.kt`:
- Around line 8-11: The AudioFileStore initialization currently ignores mkdirs()
failures; update the baseDir setup in AudioFileStore (the private val baseDir:
File = File(context.filesDir, "assignment_audio").apply { ... }) to fail fast:
after checking if (!exists()) call mkdirs() and if it returns false or the path
still isn't a directory, throw an IllegalStateException (or similar) with a
clear message including the directory path so the class does not continue with
an invalid baseDir.

In
`@mobile/app/src/main/java/com/smartjam/app/data/local/dao/SubmissionResultDao.kt`:
- Around line 16-17: The one-shot DAO query getResultsForAssignmentOnce returns
results in unspecified order while the flow variant orders by createdAt DESC;
update the SQL in the `@Query` for getResultsForAssignmentOnce (table
submission_results) to include "ORDER BY createdAt DESC" so both one-shot and
flow methods return results in the same createdAt descending order.

In `@mobile/app/src/main/java/com/smartjam/app/data/local/TokenStorage.kt`:
- Around line 23-29: saveToken currently leaves a stale USER_ROLE when role is
null and clearTokens doesn't remove USER_ROLE; update TokenStorage.kt so
saveToken(accessToken: String, refreshToken: String, role: String? = null)
deletes the USER_ROLE preference when role == null (instead of leaving the old
value) and ensure clearTokens() also explicitly removes USER_ROLE from
preferences along with ACCESS_TOKEN and REFRESH_TOKEN; reference the
ACCESS_TOKEN, REFRESH_TOKEN and USER_ROLE preference keys and the saveToken and
clearTokens functions to locate where to edit.

In `@mobile/app/src/main/java/com/smartjam/app/domain/model/Connection.kt`:
- Around line 4-10: The generated equals()/hashCode() for data class Connection
is wrong for the peerAvatarBytes array; override equals(other: Any?) and
hashCode() in Connection to compare peerAvatarBytes with
ByteArray.contentEquals(other.peerAvatarBytes) (handling nulls) and compute its
hash using peerAvatarBytes.contentHashCode() (or 0 when null), while keeping the
existing comparisons for id, peerId, peerName and peerAvatarUrl so two
Connection instances with identical avatar bytes are considered equal.

In
`@mobile/app/src/main/java/com/smartjam/app/domain/repository/AuthRepository.kt`:
- Around line 70-72: In AuthRepository's login flow remove the redundant
immediate call to refreshWithRole(role) after saving fresh tokens (the call
right after saveTokens / token persist), since valid tokens were just obtained
and the refresh adds latency and potential failure; either delete that
invocation from the login method or, if the intent was to fetch role-scoped
claims, replace it with a documented conditional or explicit separate call
(e.g., a new method getRoleClaims or a boolean flag that login accepts) so the
intent is clear—update any unit tests that assumed the refresh side‑effect
accordingly.
- Around line 143-147: The catch block in refreshWithRole() currently swallows
all Exceptions and clears tokens which breaks structured concurrency; update the
catch to rethrow CancellationException (or check if e is CancellationException
then throw) before handling other exceptions, so keep tokenStorage.clearTokens()
and apiClient.setBearerToken("") only for non-cancellation errors and return
false afterwards; reference the refreshWithRole() function and the
tokenStorage.clearTokens() / apiClient.setBearerToken("") calls when making the
change.

In
`@mobile/app/src/main/java/com/smartjam/app/domain/repository/ConnectionRepository.kt`:
- Line 29: avatarClient is created without timeouts which can cause avatar
downloads to hang; update the OkHttpClient.Builder() used for avatarClient in
ConnectionRepository.kt to set sensible timeouts (e.g., connect, read and write
timeouts) so requests fail fast on slow/unresponsive servers; modify the
avatarClient initialization (the OkHttpClient.Builder() that constructs
avatarClient) to call timeout configuration methods on the builder before
build().

In
`@mobile/app/src/main/java/com/smartjam/app/domain/repository/RoomRepository.kt`:
- Around line 41-44: The OkHttpClient instance named httpClient in
RoomRepository.kt lacks timeouts and can hang on large audio uploads/downloads;
configure sensible timeouts by updating the OkHttpClient.Builder used for
httpClient to set connectTimeout, readTimeout, writeTimeout (and optionally
callTimeout) with appropriate durations (e.g., seconds) to cover slow servers,
and keep followRedirects/followSslRedirects as-is so the client enforces bounded
wait times for audio transfer operations.
- Around line 281-284: The URL replacement logic in RoomRepository.kt
incorrectly replaces "localhost" before "references.localhost", causing
"references.localhost" to be transformed into "references.10.0.2.2" and
preventing the intended match; update the replacement order for the fixedUrl
construction (the variable fixedUrl in the repository) so the more specific
pattern "references.localhost" is replaced first, then replace "localhost" and
"127.0.0.1" afterwards (or use explicit exact-match replacements/regex to avoid
substring collisions).
- Around line 193-207: The code overwrites the server-created timestamp by using
Instant.now() for createdAt; instead, parse and use the timestamp from the
response DTO (e.g. dto.createdAt) when building SubmissionResultEntity, falling
back to Instant.now() only if the DTO timestamp is null or unparsable; update
the creation of SubmissionResultEntity (and remove the local created variable
usage) in the block that calls submissionResultDao.insertAll(...) so createdAt
is set from the DTO (after converting to java.time.Instant if necessary).
- Around line 249-254: The current download writes directly to target which can
leave partial/corrupt files if interrupted; change the logic in
RoomRepository.kt so you write the response.body?.byteStream() into a temporary
file in the same directory (e.g., target.resolveSibling(target.name + ".tmp") or
similar) using the same use/copyTo pattern, ensure streams are flushed/closed,
then atomically move/rename the temp file to the final target (using Files.move
with ATOMIC_MOVE/REPLACE_EXISTING or File.renameTo fallback) and delete the temp
on any failure; reference the existing response.body?.byteStream() read block
and the current target.outputStream() usage to locate where to swap in the
temp-file write + atomic rename logic.

In `@mobile/app/src/main/java/com/smartjam/app/MainActivity.kt`:
- Around line 103-107: The collector on tokenStorage.accessToken only sets
apiClient.setBearerToken(newToken) for non-null tokens, leaving a stale token
after logout; update the collector inside lifecycleScope.launch so that when
newToken is null you explicitly clear the client's token (e.g., call
apiClient.clearBearerToken()), otherwise set the new token with
apiClient.setBearerToken(newToken); if clearBearerToken() doesn't exist use
apiClient.setBearerToken(null) to ensure the bearer is removed.
- Around line 59-61: The OkHttp logging interceptor in MainActivity is set to
HttpLoggingInterceptor.Level.BODY unconditionally; change the configuration in
the OkHttpClient builder (where HttpLoggingInterceptor is created) to use
BuildConfig.DEBUG to choose the level (e.g., Level.BODY when BuildConfig.DEBUG
is true, otherwise Level.NONE or Level.BASIC for release) so full
request/response bodies (and sensitive headers like Authorization) are not
logged in production.
- Around line 62-65: The interceptor in MainActivity.kt currently uses
runBlocking to read tokenStorage.accessToken inside the OkHttp .addInterceptor
block; remove runBlocking and instead read from an in-memory cached token that
the DataStore collector already updates (e.g., the value set via
apiClient.setBearerToken(...) or a shared volatile/cachedToken variable updated
by the collector). Update the interceptor to check that cached token (e.g.,
cachedToken or apiClient.getBearerToken()) and only add the Authorization header
when that in-memory token is non-null and original.header("Authorization") ==
null, ensuring no blocking calls on OkHttp threads and keeping token updates
handled by the collector logic in MainActivity.

In `@mobile/app/src/main/java/com/smartjam/app/ui/navigation/NavGraph.kt`:
- Around line 107-113: The computed but unused animated value navBarPhase
(created from navBarTransition via rememberInfiniteTransition and animateFloat
in NavGraph.kt) should be removed or applied; either delete the whole
rememberInfiniteTransition/navBarPhase block to avoid unnecessary CPU work, or
reference navBarPhase in the composition where the nav bar is drawn (for example
apply it to a Modifier rotation, background brush angle, or shader) so the
animation drives a visible property; update the code that renders the nav bar to
use navBarPhase (or remove the transition) and remove any imports related to the
unused animation if you choose deletion.
- Around line 198-215: The composable for Screen.Room risks crashing on
malformed args because UUID.fromString and UserRole.valueOf can throw and
currently nulls silently return; wrap parsing of connectionIdStr and roleStr in
validation and a try/catch around UUID.fromString(...) and
UserRole.valueOf(...), and on failure either navigate back
(navController.popBackStack()) or show an error/fallback UI via RoomScreen
(e.g., pass an error state) rather than returning silently; also log the
exception. Locate the logic in the composable block that creates the
RoomViewModel with RoomViewModelFactory and calls RoomScreen to implement these
checks and error handling.

In
`@mobile/app/src/main/java/com/smartjam/app/ui/screens/login/LoginViewModel.kt`:
- Around line 30-34: The constructor parameter tokenStorage in LoginViewModel is
injected but never used; remove it from the LoginViewModel primary constructor
and from any corresponding parameter in LoginViewModelFactory (and update its
create/instantiate call sites) so the ViewModel and factory signatures match;
also remove the unused import for com.smartjam.app.data.local.TokenStorage and
rebuild to ensure no remaining references to tokenStorage remain.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomScreen.kt`:
- Around line 382-406: The division by zero happens in ErrorTimelineChart where
maxEnd is computed from teacherEndTime; guard against maxEnd being 0.0 by using
a non-zero fallback (e.g., maxEndSafe = if (maxEnd <= 0.0) 1.0 else maxEnd) or
skip drawing when maxEnd is zero, then use maxEndSafe in the calculations for
startX/endX that reference teacherStartTime and teacherEndTime so no
NaN/Infinity is produced; update the references to use the new safe denominator
(and optionally coerce startX/endX within 0..width) in the drawRect logic.
- Around line 63-77: Wrap the file copy and related operations inside the
assignmentPicker callback (the rememberLauncherForActivityResult lambda that
creates File(context.cacheDir, "temp_assignment_upload.wav")) in a try-catch so
IOExceptions (from context.contentResolver.openInputStream or
file.outputStream().use or input.copyTo) are caught; only call
viewModel.uploadAssignment when the copy succeeds, reset
pendingAssignmentTitle/Description in the success path, and in the catch block
log or surface the error (e.g., via viewModel error handler or a Toast) and
ensure any partially written temp file is deleted to avoid leaking corrupt
files.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt`:
- Around line 180-194: The code spawns unbounded coroutines in
submissions.forEach (inside viewModelScope.launch) causing duplicate concurrent
repository.getSubmissionResult calls and racey updates to
_uiState.feedbackBySubmission; fix by adding an in-flight tracker (e.g., a
mutable Set like inFlightSubmissionFetches) checked before launching a fetch for
a submission.id, add the id to the set immediately when starting the fetch and
remove it in a finally block (or on completion/error) so duplicate launches are
skipped while a request is outstanding, and ensure the add/remove are done on
the same CoroutineScope/dispatcher (or guarded by a Mutex) to avoid races when
updating the in-flight set and when updating _uiState.
- Line 40: The submissionsJobs map holds Job references that aren't cleaned up;
override RoomViewModel.onCleared() to iterate submissionsJobs.values, call
cancel() on each Job, then clear the submissionsJobs map to avoid leaks;
implement the override inside the RoomViewModel class so any running coroutines
are cancelled and submissionsJobs.reset when the ViewModel is destroyed.
- Around line 210-213: Replace the fragile string comparison of dto.status.name
with direct enum comparisons against AudioProcessingStatus: inside the launch
block in RoomViewModel where SubmissionResultResponse dto is handled, change the
checks that test status == "COMPLETED" || status == "FAILED" to compare
dto.status to AudioProcessingStatus.COMPLETED and AudioProcessingStatus.FAILED
(or use a when/contains check) so the logic uses the enum values directly.

---

Outside diff comments:
In
`@mobile/app/src/main/java/com/smartjam/app/data/local/entity/ConnectionEntity.kt`:
- Around line 9-17: The data classes ConnectionEntity and Connection currently
hold peerAvatarBytes: ByteArray?, so their auto-generated equals/hashCode
compare the byte arrays by reference; to fix, implement custom equals(other:
Any?) and hashCode() in both ConnectionEntity and Connection that compare
peerAvatarBytes using peerAvatarBytes?.contentEquals(other.peerAvatarBytes)
(handling nulls) and compute hashCode with peerAvatarBytes?.contentHashCode()
combined with other properties, or alternatively change peerAvatarBytes to an
immutable type that compares by content (e.g., List<Byte> or a ByteString) and
update any mapping code that converts between ConnectionEntity and Connection
accordingly.

In
`@mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt`:
- Around line 84-86: The error text shown when passwordRegex fails is
misleading: passwordRegex (defined near line 40) requires at least one special
character from [#?!@$%^&*-] but the _state.update call sets errorMessage to only
mention letters and digits; update the errorMessage in the failing branch (where
_state.update is called) to list the actual requirements (e.g., minimum 8
characters, Latin letters, digits, and at least one special character like
#?!@$%^&*-) so it matches passwordRegex and users know to include a special
character.
- Around line 104-110: In RegisterViewModel the failure branch after checking
result.isSuccess only updates errorMessage and never resets isLoading, causing
the UI to remain in a loading state; update the failure branch (the else after
if (result.isSuccess)) to also set _state.update { it.copy(isLoading = false,
errorMessage = error) } or move the isLoading=false logic into a shared
finally/cleanup section (similar to LoginViewModel) so _state.isLoading is
always cleared whether registration succeeds (and you send
RegisterEvent.NavigateToHome) or fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bf4c4ef-aad0-4556-8052-aa86b342aa7d

📥 Commits

Reviewing files that changed from the base of the PR and between 0161c87 and d5b78cc.

📒 Files selected for processing (37)
  • mobile/app/build.gradle.kts
  • mobile/app/src/androidTest/java/com/smartjam/app/ConnectionSeedInstrumentedTest.kt
  • mobile/app/src/main/AndroidManifest.xml
  • mobile/app/src/main/java/com/smartjam/app/MainActivity.kt
  • mobile/app/src/main/java/com/smartjam/app/api.yaml
  • mobile/app/src/main/java/com/smartjam/app/common-models.yaml
  • mobile/app/src/main/java/com/smartjam/app/data/api/AuthAuthenticator.kt
  • mobile/app/src/main/java/com/smartjam/app/data/api/InstantAdapter.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/AudioFileStore.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/SmartJamDatabase.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/TokenStorage.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/dao/AssignmentDao.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/dao/ConnectionDao.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/dao/SubmissionResultDao.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/entity/AssignmentEntity.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/entity/ConnectionEntity.kt
  • mobile/app/src/main/java/com/smartjam/app/data/local/entity/SubmissionResultEntity.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/CommentModels.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/ConnectionModels.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/LoginRequest.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/LoginResponse.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/RefreshRequest.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/RegisterRequest.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/TaskModels.kt
  • mobile/app/src/main/java/com/smartjam/app/domain/model/Connection.kt
  • mobile/app/src/main/java/com/smartjam/app/domain/repository/AuthRepository.kt
  • mobile/app/src/main/java/com/smartjam/app/domain/repository/ConnectionRepository.kt
  • mobile/app/src/main/java/com/smartjam/app/domain/repository/RoomRepository.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/navigation/NavGraph.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/home/HomeScreen.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/home/HomeViewModel.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/login/LoginScreen.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/login/LoginViewModel.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/register/RegisterViewModel.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomScreen.kt
  • mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt
  • mobile/gradlew
💤 Files with no reviewable changes (7)
  • mobile/app/src/main/java/com/smartjam/app/data/model/LoginResponse.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/ConnectionModels.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/RefreshRequest.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/LoginRequest.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/CommentModels.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/TaskModels.kt
  • mobile/app/src/main/java/com/smartjam/app/data/model/RegisterRequest.kt

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:usesCleartextTraffic="true"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not enable cleartext traffic globally.

android:usesCleartextTraffic="true" permits HTTP for all app traffic and materially weakens credential/token transport security.

Suggested hardening
-        android:usesCleartextTraffic="true"
+        android:usesCleartextTraffic="false"

Use a debug-only network security config if local HTTP is required during development.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/AndroidManifest.xml` at line 9, The manifest enables
cleartext globally via android:usesCleartextTraffic="true", which weakens
transport security; remove that global attribute and instead create a network
security config (e.g., res/xml/network_security_config.xml) that enables
cleartext only for debug builds and disallows it for release, then reference
that config from the <application> tag using
android:networkSecurityConfig="`@xml/network_security_config`" via
build-variant-specific manifests or manifest placeholders so cleartext is
allowed only in debug and not in production.

Comment on lines +531 to +534
type: array
items:
type: string
example: [ "STUDENT" ]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/api.yaml"

# 1) Show the exact region around the reported lines
echo "---- Context around lines 520-560 ----"
nl -ba "$FILE" | sed -n '500,580p'

# 2) Locate the UserResponse schema section and the specific "roles" field
echo
echo "---- Find UserResponse and roles field ----"
rg -n "UserResponse" "$FILE"
rg -n "roles:" "$FILE"

# Print the blocks near each roles occurrence
echo
echo "---- Context around each 'roles:' occurrence ----"
rg -n -C 6 "roles:" "$FILE"

# 3) Check whether UserRole schema exists and what it looks like
echo
echo "---- Find UserRole schema definition ----"
rg -n "UserRole" "$FILE"

echo
echo "---- Context around '`#/components/schemas/UserRole`' refs (if any) ----"
rg -n -C 4 "`#/components/schemas/UserRole`" "$FILE" || true

echo
echo "---- Show components/schemas section where UserRole might live ----"
# Try to find components/schemas: and then show a window around it
rg -n "components:" "$FILE"
rg -n -C 4 "schemas:" "$FILE"
# Show a reasonable window after schemas: (limit output)
start=$(rg -n "components:" "$FILE" | head -n1 | cut -d: -f1)
if [[ -n "${start:-}" ]]; then
  echo "---- Window around components->schemas ----"
  nl -ba "$FILE" | sed -n "$((start-10)),$((start+200))p" | head -n 260
fi

Repository: Satlykovs/SmartJam

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/api.yaml"

num_lines() { awk '{printf "%d\t%s\n", NR, $0}'; }

echo "---- Context around lines 500-580 (with line numbers) ----"
num_lines < "$FILE" | sed -n '500,580p'

echo
echo "---- Find UserResponse schema section ----"
rg -n "UserResponse" "$FILE" || true

echo
echo "---- Find 'roles:' occurrences ----"
rg -n "roles:" "$FILE" || true

echo
echo "---- Context around each 'roles:' occurrence ----"
# limit context output volume
rg -n -C 6 "roles:" "$FILE" || true

echo
echo "---- Find UserRole definition and refs ----"
rg -n "UserRole" "$FILE" || true
echo
rg -n -C 4 "`#/components/schemas/UserRole`" "$FILE" || true

echo
echo "---- Locate components/schemas region ----"
rg -n "components:" "$FILE" || true
rg -n "schemas:" "$FILE" || true
# Print a window after first schemas: occurrence
schemas_line=$(rg -n "schemas:" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${schemas_line:-}" ]]; then
  start=$((schemas_line-20))
  end=$((schemas_line+240))
  echo
  echo "---- Window around components/schemas (approx $start-$end) ----"
  num_lines < "$FILE" | sed -n "${start},${end}p"
fi

Repository: Satlykovs/SmartJam

Length of output: 10969


Use UserRole enum for UserResponse.roles items.

UserResponse.roles is currently array<string> (items: { type: string }), which allows invalid role values; components.schemas.UserRole already defines enum: [STUDENT, TEACHER]. Update roles.items to reference it.

Contract fix
         roles:
           type: array
           items:
-            type: string
+            $ref: '`#/components/schemas/UserRole`'
📝 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
type: array
items:
type: string
example: [ "STUDENT" ]
type: array
items:
$ref: '`#/components/schemas/UserRole`'
example: [ "STUDENT" ]
🧰 Tools
🪛 Checkov (3.2.529)

[medium] 531-537: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🪛 YAMLlint (1.38.0)

[error] 534-534: too many spaces inside brackets

(brackets)


[error] 534-534: too many spaces inside brackets

(brackets)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/api.yaml` around lines 531 - 534,
The roles array in UserResponse is typed as plain strings which allows invalid
values; change the items definition to reference the existing enum schema by
replacing the current items: { type: string } with an item $ref to the UserRole
schema (referencing components.schemas.UserRole, e.g. $ref:
'`#/components/schemas/UserRole`') so UserResponse.roles uses the UserRole enum.

Comment on lines +44 to +59
teacherStartTime:
type: number
format: double
description: Start time of the event in the teacher's reference track (seconds).
teacherEndTime:
type: number
format: double
description: End time of the event in the teacher's reference track (seconds).
studentStartTime:
type: number
format: double
description: Start time of the event in the student's submission (seconds).
studentEndTime:
type: number
format: double
description: End time of the event in the student's submission (seconds).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="mobile/app/src/main/java/com/smartjam/app/common-models.yaml"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi
echo "=== file head (first 120 lines) ==="
nl -ba "$FILE" | head -n 120
echo "=== file lines 30-90 ==="
nl -ba "$FILE" | sed -n '30,90p'

Repository: Satlykovs/SmartJam

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="mobile/app/src/main/java/com/smartjam/app/common-models.yaml"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "=== file lines 30-90 (with numbers via awk) ==="
awk 'NR>=30 && NR<=90 {printf "%5d\t%s\n", NR, $0}' "$FILE"

echo "=== locate time field keys ==="
rg -n "teacherStartTime|teacherEndTime|studentStartTime|studentEndTime" "$FILE" || true

echo "=== locate existing numeric bounds keywords ==="
rg -n "minimum:|exclusiveMinimum:|nonNegative|minimum\\b" "$FILE" || true

Repository: Satlykovs/SmartJam

Length of output: 1973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "teacherStartTime|teacherEndTime|studentStartTime|studentEndTime" . || true

echo "=== search for negative timestamp checks ==="
rg -n "negative|must be (>=|greater than|at least)|exclusiveMinimum|nonNegative|min(imum)?\\s*:\\s*[0-9]" mobile/app/src/main/java/com/smartjam/app/common-models.yaml . || true

Repository: Satlykovs/SmartJam

Length of output: 3163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== openapi-spec/common-models.yaml lines 30-80 ==="
FILE="openapi-spec/common-models.yaml"
awk 'NR>=30 && NR<=80 {printf "%5d\t%s\n", NR, $0}' "$FILE"

echo "=== openapi-spec/api.yaml lines 340-420 ==="
FILE="openapi-spec/api.yaml"
awk 'NR>=340 && NR<=420 {printf "%5d\t%s\n", NR, $0}' "$FILE"

Repository: Satlykovs/SmartJam

Length of output: 4095


🏁 Script executed:

#!/bin/bash
set -euo pipefail

A="mobile/app/src/main/java/com/smartjam/app/common-models.yaml"
B="openapi-spec/common-models.yaml"

echo "=== compare FeedbackEvent time field blocks (mobile vs openapi-spec) ==="
python3 - <<'PY'
import itertools

A="mobile/app/src/main/java/com/smartjam/app/common-models.yaml"
B="openapi-spec/common-models.yaml"

def extract(path):
    out=[]
    with open(path,'r',encoding='utf-8') as f:
        lines=f.readlines()
    # grab around teacherStartTime through studentEndTime
    for i,l in enumerate(lines):
        if "teacherStartTime:" in l:
            start=i-1
            break
    else:
        raise SystemExit("teacherStartTime not found in "+path)
    # extend until after studentEndTime description block
    end=None
    for j in range(start+1,len(lines)):
        if "studentEndTime:" in lines[j]:
            # find next 2-level indent end by stopping after description line following it
            # just take until next key at same indent level ("type:" for event) if present
            pass
    # simpler: take until line containing "type:" with two more spaces than others (event schema field)
    for j in range(start,len(lines)):
        if lines[j].startswith("        type:") or lines[j].startswith("      type:"):
            end=j
            break
    if end is None:
        end=start+40
    return "".join(lines[start:end])

print("--- mobile ---")
print(extract(A))
print("--- openapi-spec ---")
print(extract(B))
PY

Repository: Satlykovs/SmartJam

Length of output: 1493


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "teacherStartTime\\(|teacherEndTime\\(|studentStartTime\\(|studentEndTime\\(" backend mobile || true
echo "=== search for numeric guards on time fields ==="
rg -n "(StartTime|EndTime).*\\b(>=|>|greater than|non-negative|nonnegative|>=\\s*0|<\\s*0|negative)" backend mobile || true
rg -n "\\bminimum\\b|exclusiveMinimum|nonNegative" backend mobile openapi-spec || true

Repository: Satlykovs/SmartJam

Length of output: 1361


Add non-negative constraints for feedback timestamp fields

FeedbackEvent.teacherStartTime, teacherEndTime, studentStartTime, and studentEndTime are defined without any minimum, so negative timestamps are allowed by the OpenAPI schema. Ensure the same change is applied to both mobile/app/src/main/java/com/smartjam/app/common-models.yaml and openapi-spec/common-models.yaml.

Schema hardening
         teacherStartTime:
           type: number
           format: double
+          minimum: 0
         teacherEndTime:
           type: number
           format: double
+          minimum: 0
         studentStartTime:
           type: number
           format: double
+          minimum: 0
         studentEndTime:
           type: number
           format: double
+          minimum: 0
📝 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
teacherStartTime:
type: number
format: double
description: Start time of the event in the teacher's reference track (seconds).
teacherEndTime:
type: number
format: double
description: End time of the event in the teacher's reference track (seconds).
studentStartTime:
type: number
format: double
description: Start time of the event in the student's submission (seconds).
studentEndTime:
type: number
format: double
description: End time of the event in the student's submission (seconds).
teacherStartTime:
type: number
format: double
minimum: 0
description: Start time of the event in the teacher's reference track (seconds).
teacherEndTime:
type: number
format: double
minimum: 0
description: End time of the event in the teacher's reference track (seconds).
studentStartTime:
type: number
format: double
minimum: 0
description: Start time of the event in the student's submission (seconds).
studentEndTime:
type: number
format: double
minimum: 0
description: End time of the event in the student's submission (seconds).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/common-models.yaml` around lines 44
- 59, The four timestamp properties teacherStartTime, teacherEndTime,
studentStartTime, and studentEndTime allow negative values; update their OpenAPI
schema entries to enforce non-negative values by adding "minimum: 0" to each
property in the common-models YAML(s) so both copies of the schema are hardened
(ensure you modify the definitions for teacherStartTime, teacherEndTime,
studentStartTime and studentEndTime).

Comment on lines +86 to +90
private fun toApiRole(role: String): com.smartjam.app.model.UserRole? {
return try {
com.smartjam.app.model.UserRole.valueOf(role)
} catch (e: Exception) {
null
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Narrow exception handling in toApiRole.

Catching Exception is too broad here; valueOf should be guarded with IllegalArgumentException only.

Refactor
     private fun toApiRole(role: String): com.smartjam.app.model.UserRole? {
         return try {
             com.smartjam.app.model.UserRole.valueOf(role)
-        } catch (e: Exception) {
+        } catch (e: IllegalArgumentException) {
             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
private fun toApiRole(role: String): com.smartjam.app.model.UserRole? {
return try {
com.smartjam.app.model.UserRole.valueOf(role)
} catch (e: Exception) {
null
private fun toApiRole(role: String): com.smartjam.app.model.UserRole? {
return try {
com.smartjam.app.model.UserRole.valueOf(role)
} catch (e: IllegalArgumentException) {
null
}
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 89-89: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)


[warning] 89-89: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/data/api/AuthAuthenticator.kt`
around lines 86 - 90, The toApiRole function is catching Exception too broadly;
narrow it to only catch IllegalArgumentException thrown by
com.smartjam.app.model.UserRole.valueOf(role) so invalid enum names return null
without masking other errors—update the try/catch in toApiRole to catch
IllegalArgumentException (or use runCatching { UserRole.valueOf(role)
}.getOrNull()) and keep the current null-on-failure behavior.

Comment on lines +24 to +27
return try {
Instant.parse(s)
} catch (e: Exception) {
null
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/data/api/InstantAdapter.kt"

echo "== File exists =="
ls -l "$FILE" || true

echo "== Relevant lines (20-60) =="
nl -ba "$FILE" | sed -n '1,120p' | sed -n '20,60p' || true

echo "== Full file (if <=200 lines) =="
LINES=$(wc -l < "$FILE" 2>/dev/null || echo 0)
echo "Line count: $LINES"
if [ "$LINES" -le 200 ]; then
  nl -ba "$FILE"
fi

echo "== Search for InstantAdapter usage =="
rg -n "InstantAdapter" -S mobile/app/src/main/java || true

echo "== Search for parse failure handling patterns for adapters =="
rg -n "catch \\(e: Exception\\)" mobile/app/src/main/java/com/smartjam/app/data/api || true
rg -n "return null" mobile/app/src/main/java/com/smartjam/app/data/api || true

Repository: Satlykovs/SmartJam

Length of output: 378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/data/api/InstantAdapter.kt"

echo "== File exists =="
ls -l "$FILE" || true

echo "== File contents with line numbers (entire file) =="
awk '{printf "%4d:%s\n", NR, $0}' "$FILE"

echo "== Search for InstantAdapter usage =="
rg -n "InstantAdapter" mobile/app/src/main/java || true

echo "== Search for parse failure handling patterns for adapters in data/api =="
rg -n "catch \\(e: Exception\\)" mobile/app/src/main/java/com/smartjam/app/data/api || true
rg -n "return null" mobile/app/src/main/java/com/smartjam/app/data/api || true

Repository: Satlykovs/SmartJam

Length of output: 2197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "registerTypeAdapter\\(Instant::class.java, InstantAdapter\\(" -S mobile/app/src/main/java || true
rg -n "\\bInstant\\b" mobile/app/src/main/java --glob='*.kt' || true

echo "== MainActivity snippet around Gson builder =="
FILE="mobile/app/src/main/java/com/smartjam/app/MainActivity.kt"
if [ -f "$FILE" ]; then
  awk 'NR>=1 && NR<=120 {printf "%4d:%s\n", NR, $0}' "$FILE"
fi

echo "== AuthAuthenticator snippet around existing catch blocks =="
FILE="mobile/app/src/main/java/com/smartjam/app/data/api/AuthAuthenticator.kt"
if [ -f "$FILE" ]; then
  wc -l "$FILE"
  awk 'NR>=1 && NR<=140 {printf "%4d:%s\n", NR, $0}' "$FILE"
fi

Repository: Satlykovs/SmartJam

Length of output: 11995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find all occurrences of 'Instant' in model/api DTOs =="
rg -n "\\bInstant\\b" mobile/app/src/main/java/com/smartjam/app/model mobile/app/src/main/java/com/smartjam/app/api mobile/app/src/main/java/com/smartjam/app/data/api || true

echo "== List Kotlin files in model that mention Instant (for targeted reading) =="
fd -e kt ".*" mobile/app/src/main/java/com/smartjam/app/model 2>/dev/null | head -n 200 | wc -l || true
rg -l "\\bInstant\\b" mobile/app/src/main/java/com/smartjam/app/model --glob="*.kt" || true

echo "== Inspect any model property declarations with Instant around those files =="
for f in $(rg -l "\\bInstant\\b" mobile/app/src/main/java/com/smartjam/app/model --glob="*.kt"); do
  echo "--- $f ---"
  # print just regions around "Instant" lines
  rg -n "\\bInstant\\b" "$f" -n
  # show ~20 lines around first few matches
  for ln in $(rg -n "\\bInstant\\b" "$f" | head -n 5 | cut -d: -f1); do
    start=$((ln-10))
    end=$((ln+10))
    awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%4d:%s\n", NR, $0}' "$f"
  done
done

Repository: Satlykovs/SmartJam

Length of output: 1216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Occurrences of java.time.Instant in repo (checked-in sources) =="
rg -n "java\\.time\\.Instant" -S . --glob='*.kt' || true

echo "== Occurrences of 'Instant?' (nullable) in checked-in sources =="
rg -n "\\bInstant\\?" -S mobile/app/src/main/java --glob='*.kt' || true

echo "== Occurrences of 'Instant' token in checked-in sources (kt only) =="
rg -n "\\bInstant\\b" -S mobile/app/src/main/java --glob='*.kt' || true

echo "== Search for any Json TypeAdapter that swallows parse errors =="
rg -n "TypeAdapter<|catch \\(e: Exception\\)|return null" mobile/app/src/main/java/com/smartjam/app/data/api --glob='*.kt' || true

Repository: Satlykovs/SmartJam

Length of output: 4720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Tree (top 4 levels) under mobile/app/src/main/java/com/smartjam/app =="
find mobile/app/src/main/java/com/smartjam/app -maxdepth 4 -type d | sed -n '1,200p'

echo "== Find files under mobile/app/src/main/java/com/smartjam/app/api (if exists) =="
if [ -d "mobile/app/src/main/java/com/smartjam/app/api" ]; then
  find mobile/app/src/main/java/com/smartjam/app/api -maxdepth 6 -type f | sed -n '1,200p'
else
  echo "No directory: mobile/app/src/main/java/com/smartjam/app/api"
fi

echo "== Search for DTO fields named createdAt containing Instant or Instant? =="
rg -n "createdAt" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java' || true

echo "== Search for any Kotlin/Java types declaring Instant fields =="
rg -n "Instant" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java' || true

Repository: Satlykovs/SmartJam

Length of output: 5301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for createdAt property declarations (type included) =="
rg -n "createdAt\\s*[:=]" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java'

echo "== Search for DTO/domain types containing createdAt usage =="
rg -n "dto\\.createdAt" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java'

echo "== Read RoomRepository around dto.createdAt assignments =="
FILE="mobile/app/src/main/java/com/smartjam/app/domain/repository/RoomRepository.kt"
if [ -f "$FILE" ]; then
  # show around the matches lines 50-120, 140-220, 240-290 where createdAt appears
  awk 'NR>=40 && NR<=120 {printf "%4d:%s\n", NR, $0}' "$FILE"
  echo "----"
  awk 'NR>=140 && NR<=220 {printf "%4d:%s\n", NR, $0}' "$FILE"
  echo "----"
  awk 'NR>=230 && NR<=310 {printf "%4d:%s\n", NR, $0}' "$FILE"
fi

echo "== Read ConnectionRepository around dto.createdAt assignments =="
FILE="mobile/app/src/main/java/com/smartjam/app/domain/repository/ConnectionRepository.kt"
if [ -f "$FILE" ]; then
  awk 'NR>=1 && NR<=120 {printf "%4d:%s\n", NR, $0}' "$FILE"
fi

Repository: Satlykovs/SmartJam

Length of output: 18888


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  mobile/app/src/main/java/com/smartjam/app/data/local/entity/AssignmentEntity.kt \
  mobile/app/src/main/java/com/smartjam/app/data/local/entity/ConnectionEntity.kt \
  mobile/app/src/main/java/com/smartjam/app/data/local/entity/SubmissionResultEntity.kt; do
  echo "==== $f ===="
  awk 'NR>=1 && NR<=120 {printf "%4d:%s\n", NR, $0}' "$f"
done

Repository: Satlykovs/SmartJam

Length of output: 2155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find DTO response types mentioning createdAt =="
rg -n "createdAt" mobile/app/src/main/java/com/smartjam/app/data/model --glob='*.kt' --glob='*.java' || true
rg -n "createdAt" mobile/app/src/main/java/com/smartjam/app/domain/model --glob='*.kt' --glob='*.java' || true

echo "== Find AssignmentResponse* definitions =="
rg -n "class AssignmentResponse|data class AssignmentResponse|AssignmentResponse" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java' || true

echo "== Find AssignmentResponseDetailed definitions =="
rg -n "AssignmentResponseDetailed" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java' || true

echo "== Find SubmissionResultResponse definitions =="
rg -n "SubmissionResultResponse" mobile/app/src/main/java/com/smartjam/app --glob='*.kt' --glob='*.java' || true

echo "== Find Connection response DTO definitions mentioning createdAt =="
rg -n "createdAt\\s*[:=]" mobile/app/src/main/java/com/smartjam/app/data/model --glob='*.kt' --glob='*.java' || true

Repository: Satlykovs/SmartJam

Length of output: 1172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate definitions for AssignmentResponseDetailed / SubmissionResultResponse =="
rg -n "data class AssignmentResponseDetailed|class AssignmentResponseDetailed|AssignmentResponseDetailed\\b" -S mobile/app/src/main/java --glob='*.kt' --glob='*.java' || true
rg -n "data class SubmissionResultResponse|class SubmissionResultResponse|SubmissionResultResponse\\b" -S mobile/app/src/main/java --glob='*.kt' --glob='*.java' || true

echo "== Locate package com.smartjam.app.model =="
rg -n "^package com\\.smartjam\\.app\\.model\\b" -S mobile/app/src/main/java --glob='*.kt' --glob='*.java' || true

echo "== List directories under mobile/app/src/main/java/com/smartjam/app =="
find mobile/app/src/main/java/com/smartjam/app -maxdepth 2 -type d | sort

echo "== Search for 'createdAt' in any com/smartjam/app/model files (if present elsewhere) =="
rg -n "createdAt\\s*[:=]" -S mobile/app/src/main/java --glob='*.kt' --glob='*.java' | head -n 200

Repository: Satlykovs/SmartJam

Length of output: 2630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for AssignmentResponseDetailed symbol across repo ==" 
rg -n "AssignmentResponseDetailed" -S . --glob='*.kt' --glob='*.java' --glob='*.g.kt' || true

echo "== Search for SubmissionResultResponse symbol across repo ==" 
rg -n "SubmissionResultResponse" -S . --glob='*.kt' --glob='*.java' --glob='*.g.kt' || true

echo "== Search for createdAt in any *response* types ==" 
rg -n "createdAt" -S . --glob='*.kt' --glob='*.java' --glob='*.g.kt' --glob='*Response*' || true

echo "== List java/kotlin sources under mobile/app/src ==" 
find mobile/app/src -type d -maxdepth 4 | sort | head -n 200

Repository: Satlykovs/SmartJam

Length of output: 6241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the backend DTO model classes
rg -n "class AssignmentResponseDetailed" backend -S --glob='*.java' || true
rg -n "class SubmissionResultResponse" backend -S --glob='*.java' || true

echo "== Show AssignmentResponseDetailed.java (first 200 lines) =="
FILE_A=$(rg -l "class AssignmentResponseDetailed" backend --glob='*.java' | head -n 1 || true)
if [ -n "${FILE_A:-}" ] && [ -f "$FILE_A" ]; then
  echo "$FILE_A"
  head -n 200 "$FILE_A"
fi

echo "== Show SubmissionResultResponse.java (first 200 lines) =="
FILE_S=$(rg -l "class SubmissionResultResponse" backend --glob='*.java' | head -n 1 || true)
if [ -n "${FILE_S:-}" ] && [ -f "$FILE_S" ]; then
  echo "$FILE_S"
  head -n 200 "$FILE_S"
fi

Repository: Satlykovs/SmartJam

Length of output: 182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find files containing AssignmentResponseDetailed.java =="
find backend -type f -name '*AssignmentResponseDetailed*' -maxdepth 10 2>/dev/null || true
echo "== Find files containing SubmissionResultResponse.java =="
find backend -type f -name '*SubmissionResultResponse*' -maxdepth 10 2>/dev/null || true

echo "== Locate com.smartjam.api.model.AssignmentResponseDetailed source =="
rg -n "package com\\.smartjam\\.api\\.model" backend --glob='*.java' || true
rg -n "AssignmentResponseDetailed" backend --glob='*.java' | head -n 50 || true

echo "== Locate com.smartjam.api.model.SubmissionResultResponse source =="
rg -n "SubmissionResultResponse" backend --glob='*.java' | head -n 50 || true

Repository: Satlykovs/SmartJam

Length of output: 1943


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AssignmentsService.java relevant section =="
FILE="backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/service/AssignmentsService.java"
if [ -f "$FILE" ]; then
  awk 'NR>=1 && NR<=140 {printf "%4d:%s\n", NR, $0}' "$FILE" | sed -n '50,120p'
fi

echo "== SubmissionsService.java relevant section =="
FILE="backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/service/SubmissionsService.java"
if [ -f "$FILE" ]; then
  awk 'NR>=1 && NR<=160 {printf "%4d:%s\n", NR, $0}' "$FILE" | sed -n '40,130p'
fi

echo "== Inspect other instantiations for createdAt in those files =="
rg -n "createdAt" backend/smartjam-api/src/main/java/com/smartjam/smartjamapi/service/{AssignmentsService,SubmissionsService}.java || true

Repository: Satlykovs/SmartJam

Length of output: 7269


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search backend for AssignmentResponse / SubmissionResponse / Connection response DTOs =="
rg -n "class (AssignmentResponse|SubmissionResponse|ConnectionResponse)\\b" backend --glob='*.java' || true

echo "== Search backend for 'createdAt' fields in response model classes =="
rg -n "createdAt" backend --glob='*.java' | head -n 200

echo "== List files under backend/smartjam-api/src/main/java/com/smartjam/api/model (if exists) =="
if [ -d "backend/smartjam-api/src/main/java/com/smartjam/api/model" ]; then
  find backend/smartjam-api/src/main/java/com/smartjam/api/model -type f -maxdepth 3 | head -n 200
else
  echo "No such directory: backend/smartjam-api/src/main/java/com/smartjam/api/model"
fi

echo "== Search for com.smartjam.api.model package declarations in backend =="
rg -n "^package com\\.smartjam\\.api\\.model" backend --glob='*.java' || true

Repository: Satlykovs/SmartJam

Length of output: 1739


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find directories matching *com/smartjam/api/model* =="
find backend -type d -path '*com/smartjam/api/model*' 2>/dev/null | head -n 200 || true

echo "== Search for 'package com.smartjam.api.model' across backend =="
rg -n "^package com\\.smartjam\\.api\\.model" backend --glob='*.java' --glob='*.kt' || true

echo "== Search for class names without restricting to module path =="
rg -n "AssignmentResponseDetailed|SubmissionResultResponse" backend --glob='*.java' --glob='*.kt' || true

echo "== Search for model sources under backend (directories) =="
find backend -maxdepth 5 -type d -name model 2>/dev/null | head -n 200 || true

Repository: Satlykovs/SmartJam

Length of output: 1932


Fail fast on invalid Instant JSON payloads in InstantAdapter.read()

  • InstantAdapter.read() currently catches Exception and returns null, masking malformed datetime payloads; this can propagate into non-null Room createdAt fields (AssignmentEntity, ConnectionEntity, SubmissionResultEntity).
Fail-fast adapter behavior
+import com.google.gson.JsonParseException
+import java.time.format.DateTimeParseException
...
         return try {
             Instant.parse(s)
-        } catch (e: Exception) {
-            null
+        } catch (e: DateTimeParseException) {
+            throw JsonParseException("Invalid Instant: $s", 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
return try {
Instant.parse(s)
} catch (e: Exception) {
null
return try {
Instant.parse(s)
} catch (e: DateTimeParseException) {
throw JsonParseException("Invalid Instant: $s", e)
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 26-26: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)


[warning] 26-26: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/data/api/InstantAdapter.kt` around
lines 24 - 27, InstantAdapter.read() currently swallows all exceptions from
Instant.parse(s) and returns null, which can silently produce non-null Room
fields (AssignmentEntity, ConnectionEntity, SubmissionResultEntity); modify
InstantAdapter.read() to fail fast by not returning null on parse errors—catch
the specific parse exception (e.g., DateTimeParseException) or catch Exception
and rethrow a JsonDataException/IOException with a clear message including the
offending string, so deserialization fails immediately instead of propagating
nulls.

Comment on lines +63 to +77
val assignmentPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
val file = File(context.cacheDir, "temp_assignment_upload.wav")
context.contentResolver.openInputStream(it)?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
viewModel.uploadAssignment(file, pendingAssignmentTitle, pendingAssignmentDescription.ifBlank { null })
pendingAssignmentTitle = ""
pendingAssignmentDescription = ""
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing error handling for file copy operations.

If openInputStream or file write fails (e.g., disk full, permission denied), the exception propagates uncaught. Consider wrapping in a try-catch or validating the result before calling uploadAssignment.

🛡️ Suggested defensive approach
     val assignmentPicker = rememberLauncherForActivityResult(
         contract = ActivityResultContracts.GetContent()
     ) { uri: Uri? ->
         uri?.let {
-            val file = File(context.cacheDir, "temp_assignment_upload.wav")
-            context.contentResolver.openInputStream(it)?.use { input ->
-                file.outputStream().use { output ->
-                    input.copyTo(output)
+            try {
+                val file = File(context.cacheDir, "temp_assignment_upload.wav")
+                context.contentResolver.openInputStream(it)?.use { input ->
+                    file.outputStream().use { output ->
+                        input.copyTo(output)
+                    }
+                } ?: return@let
+                viewModel.uploadAssignment(file, pendingAssignmentTitle, pendingAssignmentDescription.ifBlank { null })
+            } catch (e: Exception) {
+                // Handle error, e.g. show toast or set error state
+            } finally {
+                pendingAssignmentTitle = ""
+                pendingAssignmentDescription = ""
                 }
-            }
-            viewModel.uploadAssignment(file, pendingAssignmentTitle, pendingAssignmentDescription.ifBlank { null })
-            pendingAssignmentTitle = ""
-            pendingAssignmentDescription = ""
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomScreen.kt`
around lines 63 - 77, Wrap the file copy and related operations inside the
assignmentPicker callback (the rememberLauncherForActivityResult lambda that
creates File(context.cacheDir, "temp_assignment_upload.wav")) in a try-catch so
IOExceptions (from context.contentResolver.openInputStream or
file.outputStream().use or input.copyTo) are caught; only call
viewModel.uploadAssignment when the copy succeeds, reset
pendingAssignmentTitle/Description in the success path, and in the catch block
log or surface the error (e.g., via viewModel error handler or a Toast) and
ensure any partially written temp file is deleted to avoid leaking corrupt
files.

Comment on lines +382 to +406
@Composable
private fun ErrorTimelineChart(feedback: List<FeedbackEvent>) {
val maxEnd = feedback.maxOfOrNull { it.teacherEndTime } ?: 1.0
val height = 40.dp

GlassContainer {
Column(modifier = Modifier.padding(12.dp)) {
Text("График ошибок", color = Color.White, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Canvas(modifier = Modifier.fillMaxWidth().height(height)) {
val width = size.width
feedback.forEach { event ->
val startX = (event.teacherStartTime / maxEnd).toFloat() * width
val endX = (event.teacherEndTime / maxEnd).toFloat() * width
val color = when (event.type) {
FeedbackType.WRONG_NOTE -> Color(0xFFFF5252)
FeedbackType.WRONG_RHYTHM -> Color(0xFFFFD166)
}
drawRect(
color = color,
topLeft = androidx.compose.ui.geometry.Offset(startX, 0f),
size = androidx.compose.ui.geometry.Size((endX - startX).coerceAtLeast(2f), size.height)
)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Division by zero when all teacherEndTime values are zero.

If all feedback events have teacherEndTime = 0.0, then maxEnd = 0.0, and the division at lines 394-395 produces NaN or Infinity, causing incorrect rectangle positions.

🐛 Proposed fix
 `@Composable`
 private fun ErrorTimelineChart(feedback: List<FeedbackEvent>) {
-    val maxEnd = feedback.maxOfOrNull { it.teacherEndTime } ?: 1.0
+    val maxEnd = (feedback.maxOfOrNull { it.teacherEndTime } ?: 1.0).coerceAtLeast(0.001)
     val height = 40.dp
📝 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
@Composable
private fun ErrorTimelineChart(feedback: List<FeedbackEvent>) {
val maxEnd = feedback.maxOfOrNull { it.teacherEndTime } ?: 1.0
val height = 40.dp
GlassContainer {
Column(modifier = Modifier.padding(12.dp)) {
Text("График ошибок", color = Color.White, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Canvas(modifier = Modifier.fillMaxWidth().height(height)) {
val width = size.width
feedback.forEach { event ->
val startX = (event.teacherStartTime / maxEnd).toFloat() * width
val endX = (event.teacherEndTime / maxEnd).toFloat() * width
val color = when (event.type) {
FeedbackType.WRONG_NOTE -> Color(0xFFFF5252)
FeedbackType.WRONG_RHYTHM -> Color(0xFFFFD166)
}
drawRect(
color = color,
topLeft = androidx.compose.ui.geometry.Offset(startX, 0f),
size = androidx.compose.ui.geometry.Size((endX - startX).coerceAtLeast(2f), size.height)
)
}
}
`@Composable`
private fun ErrorTimelineChart(feedback: List<FeedbackEvent>) {
val maxEnd = (feedback.maxOfOrNull { it.teacherEndTime } ?: 1.0).coerceAtLeast(0.001)
val height = 40.dp
GlassContainer {
Column(modifier = Modifier.padding(12.dp)) {
Text("График ошибок", color = Color.White, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Canvas(modifier = Modifier.fillMaxWidth().height(height)) {
val width = size.width
feedback.forEach { event ->
val startX = (event.teacherStartTime / maxEnd).toFloat() * width
val endX = (event.teacherEndTime / maxEnd).toFloat() * width
val color = when (event.type) {
FeedbackType.WRONG_NOTE -> Color(0xFFFF5252)
FeedbackType.WRONG_RHYTHM -> Color(0xFFFFD166)
}
drawRect(
color = color,
topLeft = androidx.compose.ui.geometry.Offset(startX, 0f),
size = androidx.compose.ui.geometry.Size((endX - startX).coerceAtLeast(2f), size.height)
)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomScreen.kt`
around lines 382 - 406, The division by zero happens in ErrorTimelineChart where
maxEnd is computed from teacherEndTime; guard against maxEnd being 0.0 by using
a non-zero fallback (e.g., maxEndSafe = if (maxEnd <= 0.0) 1.0 else maxEnd) or
skip drawing when maxEnd is zero, then use maxEndSafe in the calculations for
startX/endX that reference teacherStartTime and teacherEndTime so no
NaN/Infinity is produced; update the references to use the new safe denominator
(and optionally coerce startX/endX within 0..width) in the drawRect logic.

private val _uiState = kotlinx.coroutines.flow.MutableStateFlow(RoomUiState())
val uiState = _uiState.asStateFlow()

private var submissionsJobs: MutableMap<UUID, Job> = mutableMapOf()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

submissionsJobs map is never cleared, causing potential memory leak.

When the ViewModel is cleared, the stored Job references in submissionsJobs are not cleaned up. Override onCleared() to cancel all jobs and clear the map.

🐛 Proposed fix
+    override fun onCleared() {
+        super.onCleared()
+        submissionsJobs.values.forEach { it.cancel() }
+        submissionsJobs.clear()
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt`
at line 40, The submissionsJobs map holds Job references that aren't cleaned up;
override RoomViewModel.onCleared() to iterate submissionsJobs.values, call
cancel() on each Job, then clear the submissionsJobs map to avoid leaks;
implement the override inside the RoomViewModel class so any running coroutines
are cancelled and submissionsJobs.reset when the ViewModel is destroyed.

Comment on lines +180 to +194
submissions.forEach { submission ->
val hasFeedback = _uiState.value.feedbackBySubmission.containsKey(submission.id)
val needsDetailFetch = submission.pitchScore == null || submission.rhythmScore == null || !hasFeedback
if (needsDetailFetch) {
viewModelScope.launch {
val res = repository.getSubmissionResult(submission.id, assignmentId)
if (res.isSuccess) {
val dto = res.getOrNull()!!
val feedback = dto.feedback ?: emptyList()
_uiState.update { st ->
st.copy(feedbackBySubmission = st.feedbackBySubmission + (submission.id to feedback))
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Unbounded coroutine spawning can cause duplicate concurrent requests.

Every time the submissions flow emits, a new coroutine is launched for each submission needing detail fetch—even if a request is already in flight for that submission. This can lead to redundant API calls and race conditions updating feedbackBySubmission.

Track in-flight requests to avoid duplicates:

🐛 Suggested approach
+    private val fetchingSubmissionDetails = mutableSetOf<UUID>()
+
     private fun observeSubmissions(assignmentId: UUID) {
         submissionsJobs[assignmentId]?.cancel()
         submissionsJobs[assignmentId] = viewModelScope.launch {
             repository.getSubmissionsFlow(assignmentId).collect { submissions ->
                 _uiState.update { state ->
                     state.copy(
                         submissionsByAssignment = state.submissionsByAssignment + (assignmentId to submissions)
                     )
                 }
                 submissions.forEach { submission ->
                     val hasFeedback = _uiState.value.feedbackBySubmission.containsKey(submission.id)
                     val needsDetailFetch = submission.pitchScore == null || submission.rhythmScore == null || !hasFeedback
-                    if (needsDetailFetch) {
+                    if (needsDetailFetch && fetchingSubmissionDetails.add(submission.id)) {
                         viewModelScope.launch {
-                            val res = repository.getSubmissionResult(submission.id, assignmentId)
-                            if (res.isSuccess) {
-                                val dto = res.getOrNull()!!
-                                val feedback = dto.feedback ?: emptyList()
-                                _uiState.update { st ->
-                                    st.copy(feedbackBySubmission = st.feedbackBySubmission + (submission.id to feedback))
+                            try {
+                                val res = repository.getSubmissionResult(submission.id, assignmentId)
+                                if (res.isSuccess) {
+                                    val dto = res.getOrNull()!!
+                                    val feedback = dto.feedback ?: emptyList()
+                                    _uiState.update { st ->
+                                        st.copy(feedbackBySubmission = st.feedbackBySubmission + (submission.id to feedback))
+                                    }
                                 }
+                            } finally {
+                                fetchingSubmissionDetails.remove(submission.id)
                             }
                         }
                     }
                 }
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt`
around lines 180 - 194, The code spawns unbounded coroutines in
submissions.forEach (inside viewModelScope.launch) causing duplicate concurrent
repository.getSubmissionResult calls and racey updates to
_uiState.feedbackBySubmission; fix by adding an in-flight tracker (e.g., a
mutable Set like inFlightSubmissionFetches) checked before launching a fetch for
a submission.id, add the id to the set immediately when starting the fetch and
remove it in a finally block (or on completion/error) so duplicate launches are
skipped while a request is outstanding, and ensure the add/remove are done on
the same CoroutineScope/dispatcher (or guarded by a Mutex) to avoid races when
updating the in-flight set and when updating _uiState.

Comment on lines +210 to +213
val status = dto.status.name
if (status == "COMPLETED" || status == "FAILED") {
return@launch
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what type dto.status is and if SubmissionStatus enum exists
ast-grep --pattern 'enum class SubmissionStatus {
  $$$
}'

rg -n 'SubmissionStatus' --type kotlin

Repository: Satlykovs/SmartJam

Length of output: 44


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt"

echo "== Context around target lines =="
# Print around line 190-230 (adjust after seeing actual file size if needed)
nl -ba "$FILE" | sed -n '180,240p'

echo
echo "== Search for dto.status usage =="
rg -n "dto\.status" mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt

echo
echo "== Search for the exact comparison =="
rg -n 'status == "COMPLETED"|status == "FAILED"|dto\.status\.name' mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt

echo
echo "== Search for occurrences of 'COMPLETED'/'FAILED' with dto.status =="
rg -n '"COMPLETED"|"FAILED"' mobile/app/src/main/java/com/smartjam/app -S

Repository: Satlykovs/SmartJam

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt"

echo "== File length =="
wc -l "$FILE"

echo
echo "== Context around target lines (180-240) =="
# Print with line numbers using awk
awk 'NR>=180 && NR<=240 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "== Search for dto.status usage in RoomViewModel.kt =="
rg -n "dto\.status" "$FILE" || true

echo
echo "== Search for the exact comparison (status == \"COMPLETED\"/\"FAILED\") =="
rg -n 'status == "COMPLETED"|status == "FAILED"|dto\.status\.name' "$FILE" || true

echo
echo "== Search for occurrences of \"COMPLETED\"/\"FAILED\" with dto.status in app =="
rg -n '"COMPLETED"|"FAILED"' mobile/app/src/main/java/com/smartjam/app -S || true

Repository: Satlykovs/SmartJam

Length of output: 3058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate getSubmissionResult definition and its return type
rg -n "fun getSubmissionResult" -S mobile/app/src/main/java/com/smartjam/app || true
rg -n "getSubmissionResult\\(" -S mobile/app/src/main/java/com/smartjam/app || true

echo "----"

# Also search for a model/DTO containing ".status"
rg -n "\\.status\\.name" -S mobile/app/src/main/java/com/smartjam/app || true
rg -n "val status:" -S mobile/app/src/main/java/com/smartjam/app || true
rg -n "status\\s*:\\s*.*" -S mobile/app/src/main/java/com/smartjam/app || true

Repository: Satlykovs/SmartJam

Length of output: 2650


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show RoomRepository.kt around getSubmissionResult and relevant mappings
FILE="mobile/app/src/main/java/com/smartjam/app/domain/repository/RoomRepository.kt"
echo "== RoomRepository.kt length =="
wc -l "$FILE"
echo
echo "== RoomRepository.kt lines 150-240 =="
awk 'NR>=150 && NR<=240 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "== SubmissionResultEntity.kt =="
SUBE="mobile/app/src/main/java/com/smartjam/app/data/local/entity/SubmissionResultEntity.kt"
wc -l "$SUBE"
awk 'NR>=1 && NR<=120 {printf "%d:%s\n", NR, $0}' "$SUBE"

echo
echo "== Find SubmissionResultResponse type =="
rg -n "SubmissionResultResponse" mobile/app/src/main/java/com/smartjam/app -S

echo
echo "== Search for status property type definitions near models =="
# Try to locate the model file that declares SubmissionResultResponse
# We'll list files containing "class SubmissionResultResponse" or "data class SubmissionResultResponse"
rg -n "class SubmissionResultResponse|data class SubmissionResultResponse" mobile/app/src/main/java/com/smartjam/app -S || true

echo
echo "== Search for enums with COMPLETED/FAILED =="
rg -n "COMPLETED|FAILED" mobile/app/src/main/java/com/smartjam/app -S || true

Repository: Satlykovs/SmartJam

Length of output: 6503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the model file path for SubmissionResultResponse, if present
echo "== Locate SubmissionResultResponse model files =="
rg -n "class SubmissionResultResponse|data class SubmissionResultResponse|interface SubmissionResultResponse" -S mobile/app/src/main/java/com/smartjam/app || true

echo
echo "== Locate usage of SubmissionResultResponse.status =="
rg -n "SubmissionResultResponse\\s*\\.|\\.status\\.name|val status\\s*=" -S mobile/app/src/main/java/com/smartjam/app || true

echo
echo "== Search for enum(s) containing COMPLETED/FAILED =="
rg -n "enum class .*COMPLETED|COMPLETED\\s*,|FAILED\\s*," -S mobile/app/src/main/java/com/smartjam/app || true

echo
echo "== Show common-models.yaml enum definition around COMPLETED/FAILED =="
YAML="mobile/app/src/main/java/com/smartjam/app/common-models.yaml"
if [ -f "$YAML" ]; then
  rg -n "COMPLETED|FAILED" "$YAML"
  echo
  # Print a small window around those lines
  awk 'NR>=1 && NR<=80 {print}' "$YAML"
fi

Repository: Satlykovs/SmartJam

Length of output: 3019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

YAML="mobile/app/src/main/java/com/smartjam/app/api.yaml"
echo "== api.yaml: find SubmissionResultResponse schema section =="

# Print around the SubmissionResultResponse block
# We'll locate the line number first, then print a window.
ln=$(rg -n "SubmissionResultResponse:" "$YAML" | head -n 1 | cut -d: -f1 || true)
if [ -n "${ln:-}" ]; then
  start=$((ln-20))
  end=$((ln+60))
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d:%s\n", NR, $0}' "$YAML"
else
  echo "SubmissionResultResponse not found in api.yaml"
fi

echo
echo "== Search for AudioProcessingStatus usage in Kotlin sources =="

rg -n "AudioProcessingStatus" mobile/app/src/main/java/com/smartjam/app -S || true
rg -n "status\\s*=\\s*.*AudioProcessingStatus|\\.status\\.name" mobile/app/src/main/java/com/smartjam/app -S || true

echo
echo "== Search for 'status' property types referencing AudioProcessingStatus in repo =="

rg -n "status\\s*:\\s*.*AudioProcessingStatus|\\$ref: '`#/components/schemas/AudioProcessingStatus`'" "$YAML" -S || true

Repository: Satlykovs/SmartJam

Length of output: 4199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/app/src/main/java/com/smartjam/app/domain/repository/RoomRepository.kt"
echo "== RoomRepository.kt imports (lines 1-60) =="
awk 'NR>=1 && NR<=70 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "== Search Kotlin for AudioProcessingStatus symbol (non-YAML) =="
rg -n "AudioProcessingStatus" mobile/app/src/main/java/com/smartjam/app -S --type kotlin || true

echo
echo "== Search Kotlin for '.status' enum comparison patterns =="
rg -n "\.status(\s*==|\.name)" mobile/app/src/main/java/com/smartjam/app -S --type kotlin || true

Repository: Satlykovs/SmartJam

Length of output: 3975


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Try to find any file mentioning AudioProcessingStatus/COMPLETED/FAILED in Kotlin sources.
echo "== fd: candidate files containing AudioProcessingStatus in name =="
fd -a -t f "AudioProcessingStatus*" mobile/app/src/main/java || true

echo
echo "== rg: Kotlin text for 'AudioProcessingStatus' across repo =="
rg -n "AudioProcessingStatus" mobile/app/src/main/java -S || true

echo
echo "== rg: Kotlin text for 'COMPLETED'/'FAILED' enums (rough) =="
rg -n "enum class .*COMPLETED|AudioProcessingStatus\\..*(COMPLETED|FAILED)|AudioProcessingStatus\\b|COMPLETED\\b|FAILED\\b" mobile/app/src/main/java -S --type kotlin || true

echo
echo "== Locate model package directory contents (top level) =="
ls -R mobile/app/src/main/java/com/smartjam/app/model 2>/dev/null | head -n 200 || true

Repository: Satlykovs/SmartJam

Length of output: 1097


Prefer enum comparison for dto.status instead of string literals

SubmissionResultResponse.status is defined as AudioProcessingStatus, but the code compares dto.status.name to "COMPLETED"/"FAILED", which is fragile. Compare the enum values directly.

♻️ Suggested approach
-                    val status = dto.status.name
-                    if (status == "COMPLETED" || status == "FAILED") {
+                    if (dto.status == AudioProcessingStatus.COMPLETED || dto.status == AudioProcessingStatus.FAILED) {
                         return@launch
                     }
📝 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
val status = dto.status.name
if (status == "COMPLETED" || status == "FAILED") {
return@launch
}
if (dto.status == AudioProcessingStatus.COMPLETED || dto.status == AudioProcessingStatus.FAILED) {
return@launch
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/app/src/main/java/com/smartjam/app/ui/screens/room/RoomViewModel.kt`
around lines 210 - 213, Replace the fragile string comparison of dto.status.name
with direct enum comparisons against AudioProcessingStatus: inside the launch
block in RoomViewModel where SubmissionResultResponse dto is handled, change the
checks that test status == "COMPLETED" || status == "FAILED" to compare
dto.status to AudioProcessingStatus.COMPLETED and AudioProcessingStatus.FAILED
(or use a when/contains check) so the logic uses the enum values directly.

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

Labels

enhancement New feature or request Moblie Improvements in the mobile app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants