Skip to content

feat(#430): Add Debug overlay for video playback.#511

Merged
kingargyle merged 3 commits intodevelopfrom
dac_Issue430_debug_information
Jan 19, 2026
Merged

feat(#430): Add Debug overlay for video playback.#511
kingargyle merged 3 commits intodevelopfrom
dac_Issue430_debug_information

Conversation

@kingargyle
Copy link
Contributor

@kingargyle kingargyle commented Jan 19, 2026

Adds a Debug Overlay for video playback that contains general informat for the state of playback. Also fixes an issue with the app crashing when the user login can not connect to the server.

Summary by CodeRabbit

  • New Features

    • Debug overlay and toggle in video player showing codec, transcoding state and bitrate.
    • Settings API added with migration from legacy preferences.
  • UI

    • Redesigned video player controls and hidden debug overlay; retry button added to login screen; new debug toggle text.
  • Bug Fixes

    • Login error handling improved to show an error state and allow retry.
  • Tests

    • Expanded tests covering login error paths and player debug behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

Adds a Debug Overlay for video playback that contains general informat
for the state of playback.  Also fixes an issue with the app crashing
when the user login can not connect to the server.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Walkthrough

Wraps login repository calls to return Result.Error on exceptions; adds showError() to login view contract, activity UI and retry handling; introduces ExoPlayer debug overlay, debug info propagation, transcoding tracking and a debug toggle; replaces playback control layout; and expands unit/UI tests for these flows.

Changes

Cohort / File(s) Summary
Login: repository, contract, presenter, activity, layout
serenity-app/src/main/kotlin/.../LoginRepository.kt, .../LoginUserContract.kt, .../LoginUserPresenter.kt, .../LoginUserActivity.kt, serenity-app/src/main/res/layout-large/activity_login_user.xml
Repository methods now catch exceptions and return Result.Error; added showError() to view contract; presenter invokes viewState.showError() on failures; activity implements showError(), adds a hidden retry_button and retry click wiring.
ExoPlayer: contracts and presenter
serenity-app/src/main/kotlin/.../ExoplayerContract.kt, .../ExoplayerPresenter.kt
Added updateSerenityDebugInfo, toggleDebugView (view) and toggleDebugMode (presenter); presenter tracks isTranscoding, sets it in transcoderUrl, and updates view with codec/bitrate after playVideo().
ExoPlayer: activity, layout, resources
serenity-app/src/main/kotlin/.../ExoplayerVideoActivity.kt, serenity-app/src/main/res/layout-large/activity_exoplayer_video.xml, serenity-app/src/main/res/values/strings.xml
Added debug overlay layout and Serenity debug text view; integrated DebugTextViewHelper lifecycle, debug toggle wiring, bitrate extraction on track changes; added retry and toggle_debug_info strings; adjusted ExoPlayer builder buffering/diagnostics.
Playback controls: layout replacement
serenity-app/src/main/res/layout/exo_playback_control_view.xml (removed), serenity-app/src/main/res/layout/exo_player_control_view.xml (added)
Removed old playback control layout and introduced new control layout with centered control cluster, progress/time row, and right-aligned debug/subtitle/settings buttons.
Settings: DataStore migration & API
serenity-app/src/main/kotlin/.../SettingsRepository.kt
Added DataStore produceMigrations (SharedPreferencesMigration) and a set of get/set/contains/remove helpers for String/Boolean/Int keys.
Tests: login suite
serenity-app/src/test/kotlin/.../LoginRepositoryTest.kt, .../LoginUserActivityTest.kt, .../LoginUserPresenterTest.kt
Adjusted mocking style; added tests asserting repository returns Result.Error on client exceptions; added activity tests for showError() and retry; presenter tests assert showError() invoked on failures.
Tests: ExoPlayer suite
serenity-app/src/test/kotlin/.../ExoplayerPresenterTest.kt, .../ExoplayerVideoActivityTest.kt
Added tests for toggleDebugMode, transcoding detection, debug UI toggle and info display, track-change handling, buffer/config assertions; expanded test scaffolding, SharedPreferences binding, constructor mocking and teardown cleanup.
Misc (formatting/whitespace)
serenity-app/src/main/java/.../AndroidHelper.java, .../DataSourceFactoryProvider.kt, serenity-app/src/main/kotlin/.../SerenityPlayerListener.kt
Minor formatting, whitespace, and log-string spacing tweaks with no logic changes.

Sequence Diagram(s)

sequenceDiagram
  participant Presenter
  participant Activity as View
  participant Player
  participant TranscodeService as Transcode/Server

  Presenter->>Presenter: playVideo(video)
  Presenter->>TranscodeService: isDirectPlaySupportedForContainer(video)
  alt direct play supported
    Presenter->>Presenter: isTranscoding = false
    Presenter->>Player: init with directUrl
  else transcoding required
    Presenter->>TranscodeService: createTranscodeUrl(video)
    TranscodeService-->>Presenter: transcodeUrl
    Presenter->>Presenter: isTranscoding = true
    Presenter->>Player: init with transcodeUrl
  end
  Presenter->>Activity: updateSerenityDebugInfo(isTranscoding, videoCodec, audioCodec, bitrate)
  Presenter->>Player: startPlayback()
  Player-->>Activity: track/bitrate updates
  Activity->>Activity: toggle debug overlay (toggleDebugView)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A debug overlay hops into view,
Retry paws tap when networks are blue,
Transcoding hums codecs and rate,
Controls reborn, I twirl and celebrate,
I nibble logs and cheer—hop to review!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main feature being added—a debug overlay for video playback—which is the primary focus of the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@codecov
Copy link

codecov bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 71.01449% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.61%. Comparing base (ba26976) to head (42c1d60).
⚠️ Report is 6 commits behind head on develop.

Files with missing lines Patch % Lines
...serenity/ui/video/player/ExoplayerVideoActivity.kt 66.66% 4 Missing and 8 partials ⚠️
...lds/serenity/core/repository/SettingsRepository.kt 0.00% 7 Missing ⚠️
...serenity/ui/video/player/SerenityPlayerListener.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #511      +/-   ##
=============================================
+ Coverage      46.68%   48.61%   +1.93%     
- Complexity       456      473      +17     
=============================================
  Files             65       65              
  Lines           2155     2199      +44     
  Branches         300      309       +9     
=============================================
+ Hits            1006     1069      +63     
+ Misses          1032     1002      -30     
- Partials         117      128      +11     
Flag Coverage Δ
unittests 48.61% <71.01%> (+1.93%) ⬆️

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

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (6)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserActivityTest.kt (2)

43-52: setUp order should call super.setUp() first.

Per coding guidelines, setUp should call super.setUp() as the first action. Currently clearAllMocks() is called before super.setUp().

Suggested fix
     `@Before`
     override fun setUp() {
-        clearAllMocks()
         super.setUp()
+        clearAllMocks()
 
         val intent = Intent()

Based on coding guidelines: "Extend us.nineworlds.serenity.test.InjectingTest for Toothpick-based tests and override setUp with a call to super.setUp() as the first action"


54-57: Add clearAllMocks() to tearDown.

Per coding guidelines, mocks should be cleared in @After blocks using clearAllMocks().

Suggested fix
     `@After`
     fun tearDown() {
+        clearAllMocks()
         activity.finish()
     }

Based on coding guidelines: "Define setup and cleanup routines explicitly in unit tests, ensuring all mocks are cleared in @After blocks using clearAllMocks() and Toothpick.reset()"

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

67-76: setUp order should call super.setUp() first.

Per coding guidelines, setUp should call super.setUp() as the first action. Currently Dispatchers.setMain() and clearAllMocks() are called before super.setUp().

Suggested fix
     `@Before`
     override fun setUp() {
+        super.setUp()
         Dispatchers.setMain(Dispatchers.Unconfined)
         clearAllMocks()
-        super.setUp()
         presenter = spyk(ExoplayerPresenter())

Based on coding guidelines: "override setUp with a call to super.setUp() as the first action"

serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt (1)

46-57: Call super.setUp() first and move mock cleanup to @After.

The super.setUp() must be the first statement in the override to properly initialize the Toothpick scope before any presenter creation. Additionally, the @After block is incomplete—it must clear all mocks and reset Toothpick to prevent scope leakage across tests. Import toothpick.Toothpick if not already present.

♻️ Proposed fix
 `@Before`
 override fun setUp() {
-    clearAllMocks()
-    super.setUp()
-    presenter = LoginUserPresenter()
+    super.setUp()
     Dispatchers.setMain(UnconfinedTestDispatcher())
+    presenter = LoginUserPresenter()
 }
 
 `@After`
 fun tearDown() {
     Dispatchers.resetMain()
+    clearAllMocks()
+    Toothpick.reset()
 }
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (1)

106-129: Use View Binding properties for all Activity views instead of mixing with findViewById.

The Activity initializes ActivityExoplayerVideoBinding and already accesses binding.playerView and binding.root, but still uses findViewById() for debug overlay views (exo_debug_layout, exo_debug_text_view, serenity_debug_text_view). Access these views through the binding object instead for consistency and type safety. Replace the three findViewById calls at lines 127–129 and line 223 with binding properties:

-        dataLoadingContainer = findViewById(R.id.data_loading_container)
-        exoDebugLayout = findViewById(R.id.exo_debug_layout)
-        serenityDebugTextView = findViewById(R.id.serenity_debug_text_view)
+        dataLoadingContainer = binding.dataLoadingContainer
+        exoDebugLayout = binding.exoDebugLayout
+        serenityDebugTextView = binding.serenityDebugTextView
-        debugTextViewHelper = DebugTextViewHelper(player, findViewById(R.id.exo_debug_text_view))
+        debugTextViewHelper = DebugTextViewHelper(player, binding.exoDebugTextView)

Also applies to: 223, 402, 406–409

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

85-105: Call super.setUp() first in the setUp() override.

The base InjectingTest class must initialize scopes and inject dependencies before other setup operations. Move clearAllMocks() after super.setUp() to comply with the initialization order requirement.

♻️ Proposed fix
 `@Before`
 override fun setUp() {
-    clearAllMocks()
-    super.setUp()
+    super.setUp()
+    clearAllMocks()
     mockkConstructor(VideoKeyCodeHandlerDelegate::class)
🤖 Fix all issues with AI agents
In `@serenity-app/src/main/res/layout-large/activity_login_user.xml`:
- Around line 41-47: The Button with id retry_button in activity_login_user.xml
uses a hardcoded label "Retry"; add a string resource named retry in
res/values/strings.xml (e.g., <string name="retry">Retry</string>) and update
the Button's android:text to reference it (android:text="@string/retry") to
enable proper i18n.

In `@serenity-app/src/main/res/layout/exo_player_control_view.xml`:
- Around line 96-102: Replace the hardcoded contentDescription on the
ImageButton with a string resource: add a new entry named toggle_debug_info in
res/values/strings.xml (value "Toggle Debug Info") and update the ImageButton
with id @+id/exo_debug_toggle to reference `@string/toggle_debug_info` for its
contentDescription attribute so the UI text is localized.
🧹 Nitpick comments (3)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginRepositoryTest.kt (1)

40-43: Add clearAllMocks() and Toothpick.reset() to the tearDown method.

Per coding guidelines, cleanup routines should include clearAllMocks() and Toothpick.reset() in @After blocks. Currently, clearAllMocks() is only in @Before.

Suggested fix
 `@After`
 fun tearDown() {
+    clearAllMocks()
+    Toothpick.reset()
     Dispatchers.resetMain()
 }

Add the import:

import toothpick.Toothpick

Based on coding guidelines, cleanup routines should ensure all mocks are cleared in @After blocks using clearAllMocks() and Toothpick.reset().

serenity-app/src/main/res/layout/exo_player_control_view.xml (1)

60-84: Use paddingStart/paddingEnd instead of paddingLeft/paddingRight for RTL support.

The position and duration TextViews use paddingLeft/paddingRight which don't adapt to right-to-left layouts.

Suggested fix
 <TextView android:id="@id/exo_position"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textSize="14sp"
     android:textStyle="bold"
-    android:paddingLeft="4dp"
-    android:paddingRight="4dp"
+    android:paddingStart="4dp"
+    android:paddingEnd="4dp"
     android:includeFontPadding="false"
     android:textColor="#FFBEBEBE"/>
 <TextView android:id="@id/exo_duration"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textSize="14sp"
     android:textStyle="bold"
-    android:paddingLeft="4dp"
-    android:paddingRight="4dp"
+    android:paddingStart="4dp"
+    android:paddingEnd="4dp"
     android:includeFontPadding="false"
     android:textColor="#FFBEBEBE"/>
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (1)

80-85: Prefer moxyPresenter delegate over @InjectPresenter.

Use Provider + moxyPresenter and remove @InjectPresenter/@ProvidePresenter to align with the required Moxy pattern. As per coding guidelines, please use the delegate‑based injection.

♻️ Proposed refactor
-import moxy.presenter.InjectPresenter
-import moxy.presenter.ProvidePresenter
+import moxy.ktx.moxyPresenter
@@
-    `@InjectPresenter`
-    lateinit var presenter: ExoplayerPresenter
+    internal val presenter by moxyPresenter { presenterProvider.get() }
@@
-    `@ProvidePresenter`
-    fun providePresenter(): ExoplayerPresenter = presenterProvider.get()

Also applies to: 112-114

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (6)
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt (1)

46-52: Call super.setUp() before any test setup logic.
InjectingTest requires super.setUp() as the first action; move clearAllMocks() below it to keep Toothpick setup consistent.

🛠️ Proposed fix
`@Before`
override fun setUp() {
-    clearAllMocks()
-    super.setUp()
+    super.setUp()
+    clearAllMocks()
     presenter = LoginUserPresenter()
     Dispatchers.setMain(UnconfinedTestDispatcher())
}

As per coding guidelines, ensure super.setUp() is the first action.

serenity-app/src/main/kotlin/us/nineworlds/serenity/core/repository/SettingsRepository.kt (1)

76-85: Keep remove() consistent with the broadened contains() check.

contains() now checks long/float/double keys, but remove() only deletes string/boolean/int. Keys of other types will persist and contains() may keep returning true after removal attempts.

🔧 Proposed fix
 fun remove(key: String) = runBlocking {
     val stringKey = stringPreferencesKey(key)
     val booleanKey = booleanPreferencesKey(key)
     val intKey = intPreferencesKey(key)
+    val longKey = longPreferencesKey(key)
+    val floatKey = floatPreferencesKey(key)
+    val doubleKey = doublePreferencesKey(key)
     context.dataStore.edit { preferences ->
         preferences.remove(stringKey)
         preferences.remove(booleanKey)
         preferences.remove(intKey)
+        preferences.remove(longKey)
+        preferences.remove(floatKey)
+        preferences.remove(doubleKey)
     }
     Unit
 }
serenity-app/src/test/kotlin/us/nineworlds/serenity/core/repository/SettingsRepositoryTest.kt (1)

33-48: Align test lifecycle with MockK/Toothpick guidelines.

  • super.setUp() must be the first call in setUp(), before any mock setup.
  • Static mocks (mockkStatic) belong in @BeforeClass/@AfterClass, not in instance-level @Before.
  • Toothpick.reset() must be explicitly called in the test's @After block.
Suggested adjustment
+import io.mockk.unmockkStatic
+import org.junit.AfterClass
+import org.junit.BeforeClass
+import toothpick.Toothpick
+
 `@Ignore`
 class SettingsRepositoryTest : InjectingTest() {
+    companion object {
+        `@JvmStatic`
+        `@BeforeClass`
+        fun setUpClass() {
+            mockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt")
+            mockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt")
+        }
+
+        `@JvmStatic`
+        `@AfterClass`
+        fun tearDownClass() {
+            unmockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt")
+            unmockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt")
+        }
+    }
+
     `@Before`
     override fun setUp() {
-        mockkStatic("us.nineworlds.serenity.core.repository.SettingsRepositoryKt")
-        mockkStatic("androidx.datastore.preferences.core.PreferencesDataStoreKt")
+        super.setUp()
         every { mockContext.dataStore } returns mockDataStore
         every { mockDataStore.data } returns flowOf(mockPreferences)
-
-        super.setUp()
         repository = scope.getInstance(SettingsRepository::class.java)
     }

     `@After`
     fun tearDown() {
         clearAllMocks()
         closeScope()
+        Toothpick.reset()
     }
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt (1)

85-115: Move constructor mocks to @BeforeClass/@afterclass and call super.setUp() first.

Constructor mocks are global state and must live in class-level setup/teardown. The super.setUp() call must be the first statement in the instance setUp() method.

♻️ Suggested change
 companion object {
     private val mockExoPlayerPresenter = mockk<ExoplayerPresenter>(relaxed = true)
     private val mockDataSourceFactory = mockk<DataSource.Factory>(relaxed = true)
     private val mockTrackSelector = mockk<TrackSelector>(relaxed = true)
     private val mockLogger = mockk<Logger>(relaxed = true)
     private val mockPlayer = mockk<ExoPlayer>(relaxed = true)
     private val mockTimeUtil = mockk<TimeUtil>(relaxed = true)
     private val mockSharedPreferences = mockk<SharedPreferences>(relaxed = true)
+
+    `@JvmStatic`
+    `@BeforeClass`
+    fun setUpClass() {
+        mockkConstructor(VideoKeyCodeHandlerDelegate::class)
+        mockkConstructor(DefaultLoadControl.Builder::class)
+        mockkConstructor(DefaultRenderersFactory::class)
+        mockkConstructor(ExoPlayer.Builder::class)
+    }
+
+    `@JvmStatic`
+    `@AfterClass`
+    fun tearDownClass() {
+        unmockkConstructor(VideoKeyCodeHandlerDelegate::class)
+        unmockkConstructor(DefaultLoadControl.Builder::class)
+        unmockkConstructor(DefaultRenderersFactory::class)
+        unmockkConstructor(ExoPlayer.Builder::class)
+    }
 }

 `@Before`
 override fun setUp() {
-    clearAllMocks()
     super.setUp()
-    mockkConstructor(VideoKeyCodeHandlerDelegate::class)
-    mockkConstructor(DefaultLoadControl.Builder::class)
-    mockkConstructor(DefaultRenderersFactory::class)
-    mockkConstructor(ExoPlayer.Builder::class)
+    clearAllMocks()

     val mockBuilder = mockk<ExoPlayer.Builder>(relaxed = true)

 `@After`
 fun tearDown() {
     clearAllMocks()
-    unmockkConstructor(VideoKeyCodeHandlerDelegate::class)
-    unmockkConstructor(DefaultLoadControl.Builder::class)
-    unmockkConstructor(DefaultRenderersFactory::class)
-    unmockkConstructor(ExoPlayer.Builder::class)
     Toothpick.reset()
 }
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (2)

49-114: Fix presenter injection pattern and replace all findViewById with View Binding.

This Activity violates two key guidelines:

  1. Presenter injection: Replace @InjectPresenter and @ProvidePresenter with the moxyPresenter delegate pattern (used consistently in MainActivity, LoginUserActivity, and other Activities).

  2. View references: Replace all findViewById calls with properties from ActivityExoplayerVideoBinding. The Activity already uses View Binding for playerView (line 123) but then mixes findViewById calls for dataLoadingContainer, exoDebugLayout, serenityDebugTextView, and the debug helper—these should all come from the binding object.

♻️ Suggested changes
-import moxy.presenter.InjectPresenter
-import moxy.presenter.ProvidePresenter
+import moxy.ktx.moxyPresenter
@@
-    `@InjectPresenter`
-    lateinit var presenter: ExoplayerPresenter
-
     `@Inject`
     lateinit var presenterProvider: Provider<ExoplayerPresenter>
+    internal val presenter by moxyPresenter { presenterProvider.get() }
@@
-    `@ProvidePresenter`
-    fun providePresenter(): ExoplayerPresenter = presenterProvider.get()
@@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         val binding = ActivityExoplayerVideoBinding.inflate(layoutInflater)
         setContentView(binding.root)
         playerView = binding.playerView
-
-        dataLoadingContainer = findViewById(R.id.data_loading_container)
-        exoDebugLayout = findViewById(R.id.exo_debug_layout)
-        serenityDebugTextView = findViewById(R.id.serenity_debug_text_view)
+        dataLoadingContainer = binding.dataLoadingContainer
+        exoDebugLayout = binding.exoDebugLayout
+        serenityDebugTextView = binding.serenityDebugTextView

121-129: Replace findViewById with View Binding for direct layout views.

You already inflate ActivityExoplayerVideoBinding; use it for exoDebugLayout and serenityDebugTextView. However, dataLoadingContainer is inside an included layout without an android:id, so it must remain a findViewById lookup. To make it accessible via binding, add android:id="@+id/data_loading_container" to the include element in the layout XML.

♻️ Suggested change
         val binding = ActivityExoplayerVideoBinding.inflate(layoutInflater)
         setContentView(binding.root)
         playerView = binding.playerView

-        dataLoadingContainer = findViewById(R.id.data_loading_container)
-        exoDebugLayout = findViewById(R.id.exo_debug_layout)
-        serenityDebugTextView = findViewById(R.id.serenity_debug_text_view)
+        exoDebugLayout = binding.exoDebugLayout
+        serenityDebugTextView = binding.serenityDebugTextView
+        dataLoadingContainer = findViewById(R.id.data_loading_container)
🤖 Fix all issues with AI agents
In
`@serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java`:
- Around line 122-123: The call to
context.getSystemService(Context.UI_MODE_SERVICE) can return null, so update the
AndroidHelper logic around uiModeManager and the isTv computation to null-check
the returned UiModeManager before calling getCurrentModeType(); if uiModeManager
is null, set isTv to false (or a safe default) and log or handle the missing
service. Locate the uiModeManager declaration and the isTv assignment in
AndroidHelper and guard the getCurrentModeType() call with an explicit null
check to avoid a NullPointerException.

In
`@serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt`:
- Around line 60-66: Rename the test function `init preseter updates client with
port` to reflect the null-port scenario being tested (e.g., `init presenter
defaults port to 8096` or `init presenter updates client without port`) so the
name matches the behavior where `mockServer.port` is stubbed as null; update the
test function name surrounding `presenter.initPresenter(mockServer)` and the
verification of `mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/")`
accordingly to keep intent clear.
🧹 Nitpick comments (2)
serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java (1)

110-114: Consider removing the redundant Context parameter.

The method accepts a Context context parameter, but the class already has a context field (initialized in the constructor) that all other methods use. This parameter shadows the field and creates inconsistency.

Suggested refactor
-  public boolean isTclAndroidTv(Context context) {
-    String manufacturer =
-        android.os.Build.MANUFACTURER != null ? android.os.Build.MANUFACTURER.toLowerCase() : "";
-    String brand = android.os.Build.BRAND != null ? android.os.Build.BRAND.toLowerCase() : "";
-    String model = android.os.Build.MODEL != null ? android.os.Build.MODEL.toLowerCase() : "";
+  public boolean isTclAndroidTv() {
+    String manufacturer =
+        Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase() : "";
+    String brand = Build.BRAND != null ? Build.BRAND.toLowerCase() : "";
+    String model = Build.MODEL != null ? Build.MODEL.toLowerCase() : "";

Also update line 122 to use this.context:

-    UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
+    UiModeManager uiModeManager = (UiModeManager) this.context.getSystemService(Context.UI_MODE_SERVICE);

This aligns with the pattern used by other methods in this class (e.g., isAndroidTV(), isLeanbackSupported()). Additionally, use the imported Build class instead of fully qualified android.os.Build.

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

480-551: Consider limiting bitrate capture to audio/video tracks.

Right now any selected track can overwrite bitrate, so a subtitle (or audio) track may replace the intended video value. If the overlay is meant to represent playback bitrate, restrict to audio/video and/or take the max.

♻️ Suggested change
-                            if (format.bitrate > 0) {
-                                bitrate = format.bitrate
-                            }
+                            if ((trackGroup.type == C.TRACK_TYPE_VIDEO || trackGroup.type == C.TRACK_TYPE_AUDIO) &&
+                                format.bitrate > 0
+                            ) {
+                                bitrate = maxOf(bitrate, format.bitrate)
+                            }

Comment on lines +122 to +123
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential NullPointerException: getSystemService() can return null.

context.getSystemService(Context.UI_MODE_SERVICE) may return null on certain devices or configurations. Calling getCurrentModeType() without a null check will cause a crash.

Proposed fix
     // Verify it is actually a TV device (Leanback)
     UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
-    boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+    boolean isTv = uiModeManager != null 
+        && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
📝 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
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
boolean isTv = uiModeManager != null
&& uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
🤖 Prompt for AI Agents
In
`@serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java`
around lines 122 - 123, The call to
context.getSystemService(Context.UI_MODE_SERVICE) can return null, so update the
AndroidHelper logic around uiModeManager and the isTv computation to null-check
the returned UiModeManager before calling getCurrentModeType(); if uiModeManager
is null, set isTv to false (or a safe default) and log or handle the missing
service. Locate the uiModeManager declaration and the isTv assignment in
AndroidHelper and guard the getCurrentModeType() call with an explicit null
check to avoid a NullPointerException.

Comment on lines 60 to +66
fun `init preseter updates client with port`() {
every { mockServer.ipAddress } returns "192.168.0.1"
every { mockServer.port } returns null

presenter.initPresenter(mockServer)

verify { mockSerenityClient.updateBaseUrl(any()) }
verify { mockServer.ipAddress }
verify { mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/") }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename test to reflect null-port behavior.
The test name says “with port” but now stubs port as null. Consider renaming to “without port” or “defaults port to 8096” for clarity.

🤖 Prompt for AI Agents
In
`@serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/activity/login/LoginUserPresenterTest.kt`
around lines 60 - 66, Rename the test function `init preseter updates client
with port` to reflect the null-port scenario being tested (e.g., `init presenter
defaults port to 8096` or `init presenter updates client without port`) so the
name matches the behavior where `mockServer.port` is stubbed as null; update the
test function name surrounding `presenter.initPresenter(mockServer)` and the
verification of `mockSerenityClient.updateBaseUrl("http://192.168.0.1:8096/")`
accordingly to keep intent clear.

@kingargyle kingargyle merged commit 62e9ed1 into develop Jan 19, 2026
5 checks passed
@kingargyle
Copy link
Contributor Author

Closes #430

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant