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).
This commit is contained in:
Narasimha-sc committed 2026-10-19 00:46:22 +00:00
1 parent 139ce5f8cc
commit 8f25c5c73c
4 files changed
+39 -19

No files matched your search

@@ -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<BadgePurchaseOutcome> = 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
@@ -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 }
}
@@ -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<Float?>(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)
}
}
}
+1 -1
View File
@@ -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) |