android: prevent access to chat list when accepting call in locked state (#7627)

* android: prevent access to chat list when accepting call in locked state

* android: request auth on resume during a call

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: shum <github.shum@liber.li>
This commit is contained in:
Evgeny
2026-10-04 12:07:39 +01:00
committed by GitHub
co-authored by Evgeny @ SimpleX Chat shum
parent be05a46628
commit a9322ff32c
8 changed files with 35 additions and 22 deletions
@@ -36,6 +36,7 @@ import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.applyAppLocale
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
@@ -342,7 +343,10 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
chatModel.activeCallInvitation.value = null
ntfManager.cancelCallNotification()
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
acceptCall = {
AppLock.recheckAuthState()
cm.acceptIncomingCall(invitation = invitation)
},
openApp = {
val intent = Intent(context, MainActivity::class.java)
.setAction(NtfManager.OpenChatAction)
@@ -207,7 +207,7 @@ fun MainScreen() {
SwitchingUsersView()
}
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
if (unauthorized.value) {
LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth
@@ -221,7 +221,8 @@ fun MainScreen() {
SplashView(true)
ModalManager.fullscreen.showPasscodeInView()
}
} else {
}
if (!unauthorized.value || chatModel.activeCallViewIsVisible.value) {
if (chatModel.showCallView.value) {
if (appPlatform.isAndroid) {
LaunchedEffect(Unit) {
@@ -235,6 +236,8 @@ fun MainScreen() {
ActiveCallView()
}
}
}
if (!unauthorized.value) {
ModalManager.fullscreen.showOneTimePasscodeInView()
AlertManager.privacySensitive.showInView()
if (onboarding == OnboardingStage.OnboardingComplete) {
@@ -269,7 +269,11 @@ object AppLock {
fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000
fun recheckAuthState() {
if (ChatModel.showCallView.value) return
if (ChatModel.showCallView.value) {
// BiometricPrompt drops a prompt requested while MainActivity is stopped (call on lock screen), so request it again
if (userAuthorized.value == false) runAuthenticate()
return
}
val enteredBackgroundVal = enteredBackground.value
val delay = ChatController.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
@@ -1,5 +1,6 @@
package chat.simplex.common.platform
import chat.simplex.common.AppLock
import chat.simplex.common.model.*
import chat.simplex.common.views.call.RcvCallInvitation
import chat.simplex.common.views.chatlist.acceptContactRequest
@@ -96,6 +97,7 @@ abstract class NtfManager {
}
fun acceptCallAction(chatId: ChatId) {
AppLock.recheckAuthState()
chatModel.clearOverlays.value = true
val invitation = chatModel.callInvitations[chatId]
if (invitation == null) {
+1 -1
View File
@@ -128,7 +128,7 @@ Common Module (commonMain)
| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) | `object ChatModel` | 86 |
| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L102) | `class AppPreferences` | 102 |
| Platform Interface | [`Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) | `interface PlatformInterface` | 15 |
| Notification Manager | [`NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L19) | `abstract class NtfManager` | 19 |
| Notification Manager | [`NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L20) | `abstract class NtfManager` | 20 |
| Theme Manager | [`ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L18) | `object ThemeManager` | 18 |
| Android Haskell Init | [`AppCommon.android.kt`](../common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt#L33) | `fun initHaskell(packageName: String)` | 33 |
| Common Migrations | [`AppCommon.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt#L41) | `fun runMigrations()` | 41 |
+4 -4
View File
@@ -115,8 +115,8 @@ When onboarding is complete:
| `SwitchingUsersView` | User switch in progress | Loading overlay |
| Auth gate | `userAuthorized != true` | `AuthView` or `SplashView` + passcode |
| Active call | `showCallView == true` | `ActiveCallView` (desktop) or call activity (Android) |
| One-time passcode | Always | `ModalManager.fullscreen.showOneTimePasscodeInView` |
| Privacy alerts | Always | `AlertManager.privacySensitive` |
| One-time passcode | `userAuthorized == true` | `ModalManager.fullscreen.showOneTimePasscodeInView` |
| Privacy alerts | `userAuthorized == true` | `AlertManager.privacySensitive` |
| Incoming call | `activeCallInvitation != null` | `IncomingCallAlertView` |
| Shared alerts | Always | `AlertManager.shared` |
@@ -294,7 +294,7 @@ object AppLock {
### Authentication Flow
1. **MainScreen** checks `unauthorized` (derived: `userAuthorized.value != true`) at line ~135.
2. If unauthorized and not in an active call:
2. If unauthorized, including during an active call:
- Launches `AppLock.runAuthenticate()` which triggers platform-specific biometric/passcode prompt.
- On Android with system auth finishing during activity destruction, authentication is skipped.
3. If `performLA` preference is set and `laFailed` is true: shows `AuthView` with "Unlock" button.
@@ -302,7 +302,7 @@ object AppLock {
### Lock Delay
The `laLockDelay` preference controls how long after backgrounding the app requires re-authentication. When `laLockDelay == 0`, screen rotation triggers a 3-second grace period (line ~270) to prevent unnecessary re-auth. A call counts as activity: `recheckAuthState()` is a no-op while `showCallView` is true, and `CallManager.endCall()` resets `enteredBackground`, so the delay is counted from the end of the call.
The `laLockDelay` preference controls how long after backgrounding the app requires re-authentication. When `laLockDelay == 0`, screen rotation triggers a 3-second grace period (line ~270) to prevent unnecessary re-auth. A call counts as activity: `recheckAuthState()` is a no-op while `showCallView` is true, and `CallManager.endCall()` resets `enteredBackground`, so the delay is counted from the end of the call. When a call is accepted from a notification or from the Android lock screen, `recheckAuthState()` is called before `acceptIncomingCall()`, so an expired delay still locks the app.
### Lock Modes
+8 -8
View File
@@ -59,18 +59,18 @@ State transitions are driven by `WCallResponse` messages from the WebRTC layer.
## 3. Android Implementation
### 3.1 CallActivity.kt (464 lines)
### 3.1 CallActivity.kt (468 lines)
[`CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt)
A dedicated `ComponentActivity` that hosts the call UI. Key responsibilities:
- **Intent handling** ([line 64](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L64)): On `AcceptCallAction` intent, looks up the matching `RcvCallInvitation` and calls `callManager.acceptIncomingCall()`.
- **Lock screen support** ([line 160](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L160)): `unlockForIncomingCall()` uses `setShowWhenLocked(true)` / `setTurnScreenOn(true)` on API 27+, falls back to window flags on older versions. `lockAfterIncomingCall()` reverses these settings.
- **Picture-in-Picture** ([line 99](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L99)): `setPipParams()` configures PiP aspect ratio and source rect hint. On Android 12+ (`Build.VERSION_CODES.S`), auto-enter PiP is enabled for video calls. `onPictureInPictureModeChanged` toggles `activeCallViewIsCollapsed` and sends a `WCallCommand.Layout` command.
- **Permission checks** ([line 122](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L122)): Checks `RECORD_AUDIO` and conditionally `CAMERA` permissions.
- **Service binding** ([line 181](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L181)): Binds to `CallService` as a workaround for Android 12 background activity launch restrictions.
- **CallActivityView composable** ([line 208](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L208)): Renders `ActiveCallView()` when permissions are granted and a call is active. Shows `CallPermissionsView` when permissions are needed. Shows `IncomingCallLockScreenAlert` for incoming calls on the lock screen.
- **Intent handling** ([line 65](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L65)): On `AcceptCallAction` intent, looks up the matching `RcvCallInvitation` and calls `callManager.acceptIncomingCall()`.
- **Lock screen support** ([line 161](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L161)): `unlockForIncomingCall()` uses `setShowWhenLocked(true)` / `setTurnScreenOn(true)` on API 27+, falls back to window flags on older versions. `lockAfterIncomingCall()` reverses these settings.
- **Picture-in-Picture** ([line 100](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L100)): `setPipParams()` configures PiP aspect ratio and source rect hint. On Android 12+ (`Build.VERSION_CODES.S`), auto-enter PiP is enabled for video calls. `onPictureInPictureModeChanged` toggles `activeCallViewIsCollapsed` and sends a `WCallCommand.Layout` command.
- **Permission checks** ([line 123](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L123)): Checks `RECORD_AUDIO` and conditionally `CAMERA` permissions.
- **Service binding** ([line 182](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L182)): Binds to `CallService` as a workaround for Android 12 background activity launch restrictions.
- **CallActivityView composable** ([line 209](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt#L209)): Renders `ActiveCallView()` when permissions are granted and a call is active. Shows `CallPermissionsView` when permissions are needed. Shows `IncomingCallLockScreenAlert` for incoming calls on the lock screen.
### 3.2 CallService.kt (207 lines)
@@ -169,7 +169,7 @@ An in-app notification banner shown when a call invitation arrives while the app
| `CallView.kt` | [`common/src/commonMain/.../views/call/CallView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt) | 28 | `expect fun ActiveCallView()`, delivery receipt waiting |
| `CallView.android.kt` | [`common/src/androidMain/.../views/call/CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) | 891 | Android WebView WebRTC, overlay, permissions |
| `CallView.desktop.kt` | [`common/src/desktopMain/.../views/call/CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) | 308 | Desktop browser WebRTC via NanoWSD |
| `CallActivity.kt` | [`android/src/main/java/.../views/call/CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) | 464 | Android call Activity, PiP, lock screen |
| `CallActivity.kt` | [`android/src/main/java/.../views/call/CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) | 472 | Android call Activity, PiP, lock screen |
| `CallService.kt` | [`android/src/main/java/.../CallService.kt`](../../android/src/main/java/chat/simplex/app/CallService.kt) | 207 | Android foreground service for calls |
| `CallManager.kt` | [`common/src/commonMain/.../views/call/CallManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt) | 119 | Call lifecycle management |
| `WebRTC.kt` | [`common/src/commonMain/.../views/call/WebRTC.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt) | -- | `CallState` enum, `WCallCommand`, `WCallResponse` types |
@@ -35,16 +35,16 @@ The architecture uses an abstract `NtfManager` in common code with platform-spec
[`NtfManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt) (139 lines, commonMain)
The global `ntfManager` instance is declared at [line 17](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L17) and initialized by each platform at startup.
The global `ntfManager` instance is declared at [line 18](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L18) and initialized by each platform at startup.
### Concrete methods
| Method | Line | Description |
|---|---|---|
| `notifyContactConnected` | [L20](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L20) | Displays "contact connected" notification for a `Contact` |
| `notifyContactRequestReceived` | [L27](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L27) | Shows contact request notification with an "Accept" action button |
| `notifyMessageReceived` | [L38](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L38) | Conditionally shows message notification based on `ntfsEnabled`, `showNotification`, and whether user is viewing that chat |
| `acceptContactRequestAction` | [L51](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L51) | Accepts a contact request from a notification action |
| `notifyContactConnected` | [L21](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L21) | Displays "contact connected" notification for a `Contact` |
| `notifyContactRequestReceived` | [L28](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L28) | Shows contact request notification with an "Accept" action button |
| `notifyMessageReceived` | [L39](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L39) | Conditionally shows message notification based on `ntfsEnabled`, `showNotification`, and whether user is viewing that chat |
| `acceptContactRequestAction` | [L52](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L52) | Accepts a contact request from a notification action |
| `openChatAction` | [L59](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L59) | Opens a specific chat from a notification tap, switching user if needed |
| `showChatsAction` | [L74](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L74) | Opens the chat list, switching user if needed |
| `acceptCallAction` | [L88](../../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L88) | Accepts a call invitation from a notification action |