From cdd682c5aa766049d2be5bc95f8b406978b1461a Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:38:40 +0100 Subject: [PATCH] [PM-28834] bug: Setting configuration for VR devices on MainActivity (#6563) --- .../com/x8bit/bitwarden/MainActivity.kt | 11 +++++ .../com/x8bit/bitwarden/MainViewModel.kt | 12 ++++++ .../com/x8bit/bitwarden/MainViewModelTest.kt | 25 ++++++++++++ .../ui/platform/util/ActivityExtensions.kt | 40 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 ui/src/main/kotlin/com/bitwarden/ui/platform/util/ActivityExtensions.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt b/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt index a6382a6851..e7f80e6383 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.navigation.compose.NavHost import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.ui.platform.base.util.EventsEffect import com.bitwarden.ui.platform.theme.BitwardenTheme +import com.bitwarden.ui.platform.util.setHorizonOSAppLayout import com.bitwarden.ui.platform.util.setupEdgeToEdge import com.bitwarden.ui.platform.util.validate import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager @@ -212,6 +213,16 @@ class MainActivity : AppCompatActivity() { .takeIf { it } ?: super.dispatchKeyEvent(event) + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + // resize only one time at the start + if (!mainViewModel.stateFlow.value.hasResizeBeenRequested) { + setHorizonOSAppLayout { + mainViewModel.trySendAction(MainAction.Internal.ResizeHasBeenRequested) + } + } + } + @Composable private fun SetupEventsEffect(navController: NavController) { EventsEffect(viewModel = mainViewModel) { event -> diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt index 9d5d3f1445..13ab15daab 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/MainViewModel.kt @@ -98,6 +98,7 @@ class MainViewModel @Inject constructor( theme = settingsRepository.appTheme, isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed, isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled, + hasResizeBeenRequested = false, ), ) { private var specialCircumstance: SpecialCircumstance? @@ -222,6 +223,7 @@ class MainViewModel @Inject constructor( is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action) is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action) is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady() + is MainAction.Internal.ResizeHasBeenRequested -> handleResizeHasBeenRequested() } } @@ -302,6 +304,10 @@ class MainViewModel @Inject constructor( sendEvent(MainEvent.NavigateToCookieAcquisition) } + private fun handleResizeHasBeenRequested() { + mutableStateFlow.update { it.copy(hasResizeBeenRequested = true) } + } + private fun handleFirstIntentReceived(action: MainAction.ReceiveFirstIntent) { handleIntent( intent = action.intent, @@ -531,6 +537,7 @@ data class MainState( val theme: AppTheme, val isScreenCaptureAllowed: Boolean, val isDynamicColorsEnabled: Boolean, + val hasResizeBeenRequested: Boolean, ) : Parcelable { /** * Contains all feature flags that are available to the UI. @@ -648,6 +655,11 @@ sealed class MainAction { * should proceed. */ data object CookieAcquisitionReady : Internal() + + /** + * Indicates that resize has been requested on the Activity + */ + data object ResizeHasBeenRequested : Internal() } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt index 70d1bb201c..a6a9a61932 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/MainViewModelTest.kt @@ -1325,6 +1325,30 @@ class MainViewModelTest : BaseViewModelTest() { } } + @Test + fun `on handleResizeHasBeenRequested should set hasResizeBeenRequested as true`() = runTest { + val viewModel = createViewModel() + val initialState = MainState( + theme = settingsRepository.appTheme, + isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed, + isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled, + hasResizeBeenRequested = false, + ) + viewModel.stateFlow.test { + assertEquals( + initialState, + awaitItem(), + ) + viewModel.trySendAction(MainAction.Internal.ResizeHasBeenRequested) + assertEquals( + initialState.copy( + hasResizeBeenRequested = true, + ), + awaitItem(), + ) + } + } + private fun createViewModel( initialSpecialCircumstance: SpecialCircumstance? = null, ) = MainViewModel( @@ -1353,6 +1377,7 @@ private val DEFAULT_STATE: MainState = MainState( theme = AppTheme.DEFAULT, isScreenCaptureAllowed = true, isDynamicColorsEnabled = false, + hasResizeBeenRequested = false, ) private val DEFAULT_FIRST_TIME_STATE = FirstTimeState( diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/util/ActivityExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/ActivityExtensions.kt new file mode 100644 index 0000000000..09ff7cdf43 --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/util/ActivityExtensions.kt @@ -0,0 +1,40 @@ +@file:OmitFromCoverage + +package com.bitwarden.ui.platform.util + +import android.app.Activity +import android.os.Build +import com.bitwarden.annotation.OmitFromCoverage + +/** + * Requests a HorizonOS-specific window resize to 1024×640 via reflection. + * Calls [onResizeRequested] only if the resize request succeeds. + */ +@Suppress("MagicNumber", "TooGenericExceptionCaught") +fun Activity.setHorizonOSAppLayout( + onResizeRequested: () -> Unit, +) { + if (!isHorizonOSDevice()) { + return + } + window.decorView.post { + try { + val clazz = Class.forName("horizonos.view.WindowExt") + val method = clazz.getMethod( + "requestWindowResize", + android.view.Window::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ) + method.invoke(null, window, 1024, 640) + } catch (t: Throwable) { + // Not Horizon OS / API not present / request ignored by system + return@post + } + onResizeRequested() + } +} + +private fun isHorizonOSDevice(): Boolean = + Build.MANUFACTURER.equals("Oculus", ignoreCase = true) || + Build.MANUFACTURER.equals("Meta", ignoreCase = true)