From 8f25c5c73c3589c719fb4dd45e42032f8483afbc Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:22:18 +0000 Subject: [PATCH] android: stop orientation locks from stopping recording and looping migration Tap-to-stop recording locked the orientation by mapping the current rotation to portrait/landscape as if rotation 0 were portrait, which is wrong for every rotation on displays that are landscape by default, so Android changed orientation (on Waydroid: reshaped the window) and recreated MainActivity and the recording stopped. Migration forced portrait; on a display that is landscape by default the activity was recreated, the lock was released in onDispose during that recreation, and the app went back to landscape - an endless recreate loop (reproduced on Waydroid; expected on landscape-default displays). Both now lock the current orientation (SCREEN_ORIENTATION_LOCKED), which does not itself change the orientation, so the activity is not recreated. On Android in landscape, migration's progress ring is sized to the window space between the top of its box and the bottom bars instead of the window width, and its text shrinks with it, so it is visible without scrolling (very short windows keep a 120dp minimum). --- .../main/java/chat/simplex/app/SimplexApp.kt | 5 +-- .../simplex/common/platform/UI.android.kt | 9 +--- .../views/migration/MigrateFromDevice.kt | 42 ++++++++++++++++--- apps/multiplatform/spec/architecture.md | 2 +- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 9093b1427b..bd5ce61ee3 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -1,6 +1,5 @@ package chat.simplex.app -import android.annotation.SuppressLint import android.app.* import android.content.Context import chat.simplex.common.platform.Log @@ -361,14 +360,12 @@ class SimplexApp: Application(), LifecycleEventObserver { override suspend fun androidUnfinishedBadgePurchases(): List = unfinishedBadgePurchases() - @SuppressLint("SourceLockedOrientationActivity") @Composable override fun androidLockPortraitOrientation() { val context = LocalContext.current DisposableEffect(Unit) { val activity = context as? Activity ?: return@DisposableEffect onDispose {} - // Lock orientation to portrait in order to have good experience with calls - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED onDispose { // Unlock orientation activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index 9dbcb6d263..aefa85124b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -27,14 +27,7 @@ actual fun LockToCurrentOrientationUntilDispose() { val context = LocalContext1.current DisposableEffect(Unit) { val activity = (context as Activity?) ?: return@DisposableEffect onDispose {} - val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager - val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation - activity.requestedOrientation = when (rotation) { - Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT - Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED // Unlock orientation onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 05c22155b8..013ad0eb25 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -11,6 +11,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight @@ -29,6 +33,7 @@ import chat.simplex.common.views.database.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.LinkTextView import chat.simplex.common.views.newchat.SimpleXLinkQRCode +import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* import chat.simplex.res.* import dev.icerock.moko.resources.compose.painterResource @@ -427,19 +432,44 @@ private fun ProgressView() { @Composable fun LargeProgressView(value: Float, title: String, description: String) { - Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) { + val landscape = !appPlatform.isDesktop && windowWidth() > windowHeight() + val scrollState = LocalAppBarHandler.current?.scrollState + val density = LocalDensity.current + val windowPx = LocalWindowInfo.current.containerSize.height + val oneHandUI = remember { derivedStateOf { appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete && appPrefs.oneHandUI.state.value } } + val bottomBarsPx = if (oneHandUI.value) { + WindowInsets.navigationBars.getBottom(density) + with(density) { (AppBarHeight * fontSizeSqrtMultiplier).roundToPx() } + } else { + WindowInsets.systemBars.getBottom(density) + } + var contentTopPx by remember { mutableStateOf(null) } + val size = when { + appPlatform.isDesktop -> DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + landscape -> contentTopPx?.let { top -> + (with(density) { (windowPx - top - bottomBarsPx).toDp() } - DEFAULT_PADDING * 2).coerceAtLeast(120.dp) + } ?: 120.dp + else -> windowWidth() - DEFAULT_PADDING * 2 + } + Box( + Modifier + .onGloballyPositioned { if (landscape) contentTopPx = it.positionInWindow().y + (scrollState?.value ?: 0) } + .padding(DEFAULT_PADDING) + .then(if (landscape) Modifier.fillMaxWidth() else Modifier.fillMaxSize()), + contentAlignment = Alignment.Center + ) { CircularProgressIndicator( progress = value, - (if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else Modifier.size(windowWidth() - DEFAULT_PADDING * - 2)) + Modifier.size(size) .rotate(-90f), color = MaterialTheme.colors.primary, strokeWidth = 25.dp ) + val textScale = if (landscape) (size / 300.dp).coerceIn(0.4f, 1f) else 1f + val descriptionStyle = MaterialTheme.typography.subtitle1.let { it.copy(fontSize = it.fontSize * textScale) } Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(description, color = Color.Transparent) - Text(title, style = MaterialTheme.typography.h1.copy(fontSize = 50.sp, fontWeight = FontWeight.Bold), color = MaterialTheme.colors.primary) - Text(description, style = MaterialTheme.typography.subtitle1) + Text(description, style = LocalTextStyle.current.let { it.copy(fontSize = it.fontSize * textScale) }, color = Color.Transparent) + Text(title, style = MaterialTheme.typography.h1.copy(fontSize = 50.sp * textScale, fontWeight = FontWeight.Bold), color = MaterialTheme.colors.primary) + Text(description, style = descriptionStyle) } } } diff --git a/apps/multiplatform/spec/architecture.md b/apps/multiplatform/spec/architecture.md index cdc43a794a..1ac6bbcc24 100644 --- a/apps/multiplatform/spec/architecture.md +++ b/apps/multiplatform/spec/architecture.md @@ -380,7 +380,7 @@ var platform: PlatformInterface = object : PlatformInterface {} | `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` | | `androidIsPlayStoreBuild` | `false` | `BuildConfig.PLAY_STORE` | | `androidLoadPlayStoreCountry()` | no-op | Request the Play account country (google flavor only) | -| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` | +| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_LOCKED` (current orientation) | | `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog | | `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) |